├── .github └── workflows │ ├── swift-build.yml │ └── swift-linter.yml ├── .gitignore ├── Example ├── SCNBezier-Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── SCNBezier-Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── BezierPoint.swift │ ├── Info.plist │ ├── SCNGeometry+Extensions.swift │ ├── ViewController+ARSCNViewDelegate.swift │ ├── ViewController+Bezier.swift │ ├── ViewController+Gestures.swift │ ├── ViewController+UI.swift │ └── ViewController.swift ├── LICENSE ├── Package.swift ├── README.md ├── SCNBezier.podspec ├── Sources └── SCNBezier │ ├── InterpolatorFunctions.swift │ ├── SCNAction+Extensions.swift │ ├── SCNBezierPath.swift │ └── SCNVector3+Extensions.swift ├── Tests └── SCNBezierTests │ └── SCNBezierTests.swift ├── install_swiftlint.sh ├── media └── bezier-example.gif └── old.travis.yml /.github/workflows/swift-build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.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: 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 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | *.xcodeproj 25 | !SCNBezier-Example.xcodeproj 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | .swiftpm/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 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 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F37C50EC22D12D890055F2ED /* SCNBezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37C50E822D12D890055F2ED /* SCNBezierPath.swift */; }; 11 | F37C50ED22D12D890055F2ED /* SCNAction+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37C50E922D12D890055F2ED /* SCNAction+Extensions.swift */; }; 12 | F37C50EE22D12D890055F2ED /* InterpolatorFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37C50EA22D12D890055F2ED /* InterpolatorFunctions.swift */; }; 13 | F37C50EF22D12D890055F2ED /* SCNVector3+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37C50EB22D12D890055F2ED /* SCNVector3+Extensions.swift */; }; 14 | F3AE2DBC22D0FEB800D5B012 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AE2DBB22D0FEB800D5B012 /* AppDelegate.swift */; }; 15 | F3AE2DC022D0FEB800D5B012 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AE2DBF22D0FEB800D5B012 /* ViewController.swift */; }; 16 | F3AE2DC222D0FEB800D5B012 /* ViewController+ARSCNViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AE2DC122D0FEB800D5B012 /* ViewController+ARSCNViewDelegate.swift */; }; 17 | F3AE2DC422D0FEB900D5B012 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3AE2DC322D0FEB900D5B012 /* Assets.xcassets */; }; 18 | F3AE2DC722D0FEB900D5B012 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F3AE2DC522D0FEB900D5B012 /* LaunchScreen.storyboard */; }; 19 | F3DA308322D1028700F2BF0A /* ViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DA308222D1028700F2BF0A /* ViewController+UI.swift */; }; 20 | F3DA308522D102AC00F2BF0A /* ViewController+Gestures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DA308422D102AC00F2BF0A /* ViewController+Gestures.swift */; }; 21 | F3DA308722D102D600F2BF0A /* BezierPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DA308622D102D600F2BF0A /* BezierPoint.swift */; }; 22 | F3DA308922D1033C00F2BF0A /* ViewController+Bezier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DA308822D1033C00F2BF0A /* ViewController+Bezier.swift */; }; 23 | F3DA308B22D1037F00F2BF0A /* SCNGeometry+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DA308A22D1037F00F2BF0A /* SCNGeometry+Extensions.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | F37C50E822D12D890055F2ED /* SCNBezierPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SCNBezierPath.swift; path = ../../Sources/SCNBezier/SCNBezierPath.swift; sourceTree = ""; }; 28 | F37C50E922D12D890055F2ED /* SCNAction+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SCNAction+Extensions.swift"; path = "../../Sources/SCNBezier/SCNAction+Extensions.swift"; sourceTree = ""; }; 29 | F37C50EA22D12D890055F2ED /* InterpolatorFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InterpolatorFunctions.swift; path = ../../Sources/SCNBezier/InterpolatorFunctions.swift; sourceTree = ""; }; 30 | F37C50EB22D12D890055F2ED /* SCNVector3+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SCNVector3+Extensions.swift"; path = "../../Sources/SCNBezier/SCNVector3+Extensions.swift"; sourceTree = ""; }; 31 | F3AE2DB822D0FEB800D5B012 /* SCNBezier-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SCNBezier-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | F3AE2DBB22D0FEB800D5B012 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33 | F3AE2DBF22D0FEB800D5B012 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 34 | F3AE2DC122D0FEB800D5B012 /* ViewController+ARSCNViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+ARSCNViewDelegate.swift"; sourceTree = ""; }; 35 | F3AE2DC322D0FEB900D5B012 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 36 | F3AE2DC622D0FEB900D5B012 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 37 | F3AE2DC822D0FEB900D5B012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | F3DA308222D1028700F2BF0A /* ViewController+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+UI.swift"; sourceTree = ""; }; 39 | F3DA308422D102AC00F2BF0A /* ViewController+Gestures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Gestures.swift"; sourceTree = ""; }; 40 | F3DA308622D102D600F2BF0A /* BezierPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierPoint.swift; sourceTree = ""; }; 41 | F3DA308822D1033C00F2BF0A /* ViewController+Bezier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Bezier.swift"; sourceTree = ""; }; 42 | F3DA308A22D1037F00F2BF0A /* SCNGeometry+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SCNGeometry+Extensions.swift"; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | F3AE2DB522D0FEB800D5B012 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | F37C50E722D12D630055F2ED /* SCNBezier */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | F37C50EA22D12D890055F2ED /* InterpolatorFunctions.swift */, 60 | F37C50E922D12D890055F2ED /* SCNAction+Extensions.swift */, 61 | F37C50E822D12D890055F2ED /* SCNBezierPath.swift */, 62 | F37C50EB22D12D890055F2ED /* SCNVector3+Extensions.swift */, 63 | ); 64 | name = SCNBezier; 65 | sourceTree = ""; 66 | }; 67 | F3AE2DAF22D0FEB800D5B012 = { 68 | isa = PBXGroup; 69 | children = ( 70 | F3AE2DBA22D0FEB800D5B012 /* SCNBezier-Example */, 71 | F3AE2DB922D0FEB800D5B012 /* Products */, 72 | ); 73 | sourceTree = ""; 74 | }; 75 | F3AE2DB922D0FEB800D5B012 /* Products */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | F3AE2DB822D0FEB800D5B012 /* SCNBezier-Example.app */, 79 | ); 80 | name = Products; 81 | sourceTree = ""; 82 | }; 83 | F3AE2DBA22D0FEB800D5B012 /* SCNBezier-Example */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | F3AE2DBB22D0FEB800D5B012 /* AppDelegate.swift */, 87 | F3AE2DBF22D0FEB800D5B012 /* ViewController.swift */, 88 | F3AE2DC122D0FEB800D5B012 /* ViewController+ARSCNViewDelegate.swift */, 89 | F3DA308822D1033C00F2BF0A /* ViewController+Bezier.swift */, 90 | F3DA308422D102AC00F2BF0A /* ViewController+Gestures.swift */, 91 | F3DA308222D1028700F2BF0A /* ViewController+UI.swift */, 92 | F3DA308A22D1037F00F2BF0A /* SCNGeometry+Extensions.swift */, 93 | F3DA308622D102D600F2BF0A /* BezierPoint.swift */, 94 | F3AE2DC322D0FEB900D5B012 /* Assets.xcassets */, 95 | F3AE2DC522D0FEB900D5B012 /* LaunchScreen.storyboard */, 96 | F3AE2DC822D0FEB900D5B012 /* Info.plist */, 97 | F37C50E722D12D630055F2ED /* SCNBezier */, 98 | ); 99 | path = "SCNBezier-Example"; 100 | sourceTree = ""; 101 | }; 102 | /* End PBXGroup section */ 103 | 104 | /* Begin PBXNativeTarget section */ 105 | F3AE2DB722D0FEB800D5B012 /* SCNBezier-Example */ = { 106 | isa = PBXNativeTarget; 107 | buildConfigurationList = F3AE2DE122D0FEB900D5B012 /* Build configuration list for PBXNativeTarget "SCNBezier-Example" */; 108 | buildPhases = ( 109 | F3AE2DB422D0FEB800D5B012 /* Sources */, 110 | F3AE2DB522D0FEB800D5B012 /* Frameworks */, 111 | F3AE2DB622D0FEB800D5B012 /* Resources */, 112 | F3DA309222D10A2A00F2BF0A /* SwiftLint */, 113 | ); 114 | buildRules = ( 115 | ); 116 | dependencies = ( 117 | ); 118 | name = "SCNBezier-Example"; 119 | packageProductDependencies = ( 120 | ); 121 | productName = "SCNBezier-Example"; 122 | productReference = F3AE2DB822D0FEB800D5B012 /* SCNBezier-Example.app */; 123 | productType = "com.apple.product-type.application"; 124 | }; 125 | /* End PBXNativeTarget section */ 126 | 127 | /* Begin PBXProject section */ 128 | F3AE2DB022D0FEB800D5B012 /* Project object */ = { 129 | isa = PBXProject; 130 | attributes = { 131 | LastSwiftUpdateCheck = 1100; 132 | LastUpgradeCheck = 1100; 133 | ORGANIZATIONNAME = "Max Cobb"; 134 | TargetAttributes = { 135 | F3AE2DB722D0FEB800D5B012 = { 136 | CreatedOnToolsVersion = 11.0; 137 | }; 138 | }; 139 | }; 140 | buildConfigurationList = F3AE2DB322D0FEB800D5B012 /* Build configuration list for PBXProject "SCNBezier-Example" */; 141 | compatibilityVersion = "Xcode 9.3"; 142 | developmentRegion = en; 143 | hasScannedForEncodings = 0; 144 | knownRegions = ( 145 | en, 146 | Base, 147 | ); 148 | mainGroup = F3AE2DAF22D0FEB800D5B012; 149 | packageReferences = ( 150 | ); 151 | productRefGroup = F3AE2DB922D0FEB800D5B012 /* Products */; 152 | projectDirPath = ""; 153 | projectRoot = ""; 154 | targets = ( 155 | F3AE2DB722D0FEB800D5B012 /* SCNBezier-Example */, 156 | ); 157 | }; 158 | /* End PBXProject section */ 159 | 160 | /* Begin PBXResourcesBuildPhase section */ 161 | F3AE2DB622D0FEB800D5B012 /* Resources */ = { 162 | isa = PBXResourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | F3AE2DC722D0FEB900D5B012 /* LaunchScreen.storyboard in Resources */, 166 | F3AE2DC422D0FEB900D5B012 /* Assets.xcassets in Resources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXResourcesBuildPhase section */ 171 | 172 | /* Begin PBXShellScriptBuildPhase section */ 173 | F3DA309222D10A2A00F2BF0A /* SwiftLint */ = { 174 | isa = PBXShellScriptBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | ); 178 | inputFileListPaths = ( 179 | ); 180 | inputPaths = ( 181 | ); 182 | name = SwiftLint; 183 | outputFileListPaths = ( 184 | ); 185 | outputPaths = ( 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | shellPath = /bin/sh; 189 | shellScript = "#!/bin/sh\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, install with `brew install swiftlint`\"\nfi\n"; 190 | }; 191 | /* End PBXShellScriptBuildPhase section */ 192 | 193 | /* Begin PBXSourcesBuildPhase section */ 194 | F3AE2DB422D0FEB800D5B012 /* Sources */ = { 195 | isa = PBXSourcesBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | F3AE2DC222D0FEB800D5B012 /* ViewController+ARSCNViewDelegate.swift in Sources */, 199 | F3DA308722D102D600F2BF0A /* BezierPoint.swift in Sources */, 200 | F37C50EC22D12D890055F2ED /* SCNBezierPath.swift in Sources */, 201 | F3DA308B22D1037F00F2BF0A /* SCNGeometry+Extensions.swift in Sources */, 202 | F3AE2DC022D0FEB800D5B012 /* ViewController.swift in Sources */, 203 | F3DA308922D1033C00F2BF0A /* ViewController+Bezier.swift in Sources */, 204 | F37C50ED22D12D890055F2ED /* SCNAction+Extensions.swift in Sources */, 205 | F37C50EF22D12D890055F2ED /* SCNVector3+Extensions.swift in Sources */, 206 | F37C50EE22D12D890055F2ED /* InterpolatorFunctions.swift in Sources */, 207 | F3DA308322D1028700F2BF0A /* ViewController+UI.swift in Sources */, 208 | F3DA308522D102AC00F2BF0A /* ViewController+Gestures.swift in Sources */, 209 | F3AE2DBC22D0FEB800D5B012 /* AppDelegate.swift in Sources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXSourcesBuildPhase section */ 214 | 215 | /* Begin PBXVariantGroup section */ 216 | F3AE2DC522D0FEB900D5B012 /* LaunchScreen.storyboard */ = { 217 | isa = PBXVariantGroup; 218 | children = ( 219 | F3AE2DC622D0FEB900D5B012 /* Base */, 220 | ); 221 | name = LaunchScreen.storyboard; 222 | sourceTree = ""; 223 | }; 224 | /* End PBXVariantGroup section */ 225 | 226 | /* Begin XCBuildConfiguration section */ 227 | F3AE2DDF22D0FEB900D5B012 /* Debug */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 234 | CLANG_CXX_LIBRARY = "libc++"; 235 | CLANG_ENABLE_MODULES = YES; 236 | CLANG_ENABLE_OBJC_ARC = YES; 237 | CLANG_ENABLE_OBJC_WEAK = YES; 238 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 239 | CLANG_WARN_BOOL_CONVERSION = YES; 240 | CLANG_WARN_COMMA = YES; 241 | CLANG_WARN_CONSTANT_CONVERSION = YES; 242 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 243 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 244 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 245 | CLANG_WARN_EMPTY_BODY = YES; 246 | CLANG_WARN_ENUM_CONVERSION = YES; 247 | CLANG_WARN_INFINITE_RECURSION = YES; 248 | CLANG_WARN_INT_CONVERSION = YES; 249 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 250 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 251 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 252 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 253 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 254 | CLANG_WARN_STRICT_PROTOTYPES = YES; 255 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 256 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 257 | CLANG_WARN_UNREACHABLE_CODE = YES; 258 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 259 | COPY_PHASE_STRIP = NO; 260 | DEBUG_INFORMATION_FORMAT = dwarf; 261 | ENABLE_STRICT_OBJC_MSGSEND = YES; 262 | ENABLE_TESTABILITY = YES; 263 | GCC_C_LANGUAGE_STANDARD = gnu11; 264 | GCC_DYNAMIC_NO_PIC = NO; 265 | GCC_NO_COMMON_BLOCKS = YES; 266 | GCC_OPTIMIZATION_LEVEL = 0; 267 | GCC_PREPROCESSOR_DEFINITIONS = ( 268 | "DEBUG=1", 269 | "$(inherited)", 270 | ); 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 278 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 279 | MTL_FAST_MATH = YES; 280 | ONLY_ACTIVE_ARCH = YES; 281 | SDKROOT = iphoneos; 282 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 283 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 284 | }; 285 | name = Debug; 286 | }; 287 | F3AE2DE022D0FEB900D5B012 /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ALWAYS_SEARCH_USER_PATHS = NO; 291 | CLANG_ANALYZER_NONNULL = YES; 292 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 294 | CLANG_CXX_LIBRARY = "libc++"; 295 | CLANG_ENABLE_MODULES = YES; 296 | CLANG_ENABLE_OBJC_ARC = YES; 297 | CLANG_ENABLE_OBJC_WEAK = YES; 298 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 299 | CLANG_WARN_BOOL_CONVERSION = YES; 300 | CLANG_WARN_COMMA = YES; 301 | CLANG_WARN_CONSTANT_CONVERSION = YES; 302 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 303 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 304 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 305 | CLANG_WARN_EMPTY_BODY = YES; 306 | CLANG_WARN_ENUM_CONVERSION = YES; 307 | CLANG_WARN_INFINITE_RECURSION = YES; 308 | CLANG_WARN_INT_CONVERSION = YES; 309 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 310 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 311 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 312 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 313 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 314 | CLANG_WARN_STRICT_PROTOTYPES = YES; 315 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 316 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 317 | CLANG_WARN_UNREACHABLE_CODE = YES; 318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 319 | COPY_PHASE_STRIP = NO; 320 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 321 | ENABLE_NS_ASSERTIONS = NO; 322 | ENABLE_STRICT_OBJC_MSGSEND = YES; 323 | GCC_C_LANGUAGE_STANDARD = gnu11; 324 | GCC_NO_COMMON_BLOCKS = YES; 325 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 326 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 327 | GCC_WARN_UNDECLARED_SELECTOR = YES; 328 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 329 | GCC_WARN_UNUSED_FUNCTION = YES; 330 | GCC_WARN_UNUSED_VARIABLE = YES; 331 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 332 | MTL_ENABLE_DEBUG_INFO = NO; 333 | MTL_FAST_MATH = YES; 334 | SDKROOT = iphoneos; 335 | SWIFT_COMPILATION_MODE = wholemodule; 336 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 337 | VALIDATE_PRODUCT = YES; 338 | }; 339 | name = Release; 340 | }; 341 | F3AE2DE222D0FEB900D5B012 /* Debug */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | CODE_SIGN_STYLE = Automatic; 346 | DEVELOPMENT_TEAM = ""; 347 | INFOPLIST_FILE = "SCNBezier-Example/Info.plist"; 348 | LD_RUNPATH_SEARCH_PATHS = ( 349 | "$(inherited)", 350 | "@executable_path/Frameworks", 351 | ); 352 | PRODUCT_BUNDLE_IDENTIFIER = ""; 353 | PRODUCT_NAME = "$(TARGET_NAME)"; 354 | SWIFT_VERSION = 5.0; 355 | TARGETED_DEVICE_FAMILY = "1,2"; 356 | }; 357 | name = Debug; 358 | }; 359 | F3AE2DE322D0FEB900D5B012 /* Release */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 363 | CODE_SIGN_STYLE = Automatic; 364 | DEVELOPMENT_TEAM = ""; 365 | INFOPLIST_FILE = "SCNBezier-Example/Info.plist"; 366 | LD_RUNPATH_SEARCH_PATHS = ( 367 | "$(inherited)", 368 | "@executable_path/Frameworks", 369 | ); 370 | PRODUCT_BUNDLE_IDENTIFIER = ""; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | SWIFT_VERSION = 5.0; 373 | TARGETED_DEVICE_FAMILY = "1,2"; 374 | }; 375 | name = Release; 376 | }; 377 | /* End XCBuildConfiguration section */ 378 | 379 | /* Begin XCConfigurationList section */ 380 | F3AE2DB322D0FEB800D5B012 /* Build configuration list for PBXProject "SCNBezier-Example" */ = { 381 | isa = XCConfigurationList; 382 | buildConfigurations = ( 383 | F3AE2DDF22D0FEB900D5B012 /* Debug */, 384 | F3AE2DE022D0FEB900D5B012 /* Release */, 385 | ); 386 | defaultConfigurationIsVisible = 0; 387 | defaultConfigurationName = Release; 388 | }; 389 | F3AE2DE122D0FEB900D5B012 /* Build configuration list for PBXNativeTarget "SCNBezier-Example" */ = { 390 | isa = XCConfigurationList; 391 | buildConfigurations = ( 392 | F3AE2DE222D0FEB900D5B012 /* Debug */, 393 | F3AE2DE322D0FEB900D5B012 /* Release */, 394 | ); 395 | defaultConfigurationIsVisible = 0; 396 | defaultConfigurationName = Release; 397 | }; 398 | /* End XCConfigurationList section */ 399 | }; 400 | rootObject = F3AE2DB022D0FEB800D5B012 /* Project object */; 401 | } 402 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 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 | func applicationWillResignActive(_ application: UIApplication) { 28 | } 29 | 30 | func applicationDidEnterBackground(_ application: UIApplication) { 31 | } 32 | 33 | func applicationWillEnterForeground(_ application: UIApplication) { 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Example/SCNBezier-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 | } -------------------------------------------------------------------------------- /Example/SCNBezier-Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/SCNBezier-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 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/BezierPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BezierPoint.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | // import SCNBezier 11 | 12 | extension Array where Element: BezierPoint { 13 | func getBezierPath() -> SCNBezierPath { 14 | let positions = self.map { $0.parent!.convertPosition($0.position, to: nil) } 15 | return SCNBezierPath(points: positions) 16 | } 17 | func getBezierVertices(count: Int = 100) -> [SCNVector3] { 18 | let bezPath = self.getBezierPath() 19 | return bezPath.getNPoints(count: count) 20 | } 21 | func getBezierGeometry(with count: Int = 100) -> SCNGeometry { 22 | let points = self.getBezierVertices(count: count) 23 | return SCNGeometry.line(points: points) 24 | } 25 | } 26 | 27 | class BezierPoint: SCNNode { 28 | init(at position: SCNVector3) { 29 | super.init() 30 | self.geometry = SCNSphere(radius: 0.05) 31 | self.geometry?.firstMaterial?.diffuse.contents = UIColor.darkGray 32 | self.position = position 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 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/SCNBezier-Example/SCNGeometry+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCNGeometry+Extensions.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit.SCNGeometry 10 | 11 | extension SCNGeometry { 12 | static func line(points: [SCNVector3], isDotted: Bool = false) -> SCNGeometry { 13 | let src = SCNGeometrySource(vertices: points) 14 | let pointSize = points.count 15 | var indices: [UInt32]! = nil 16 | if isDotted { 17 | indices = Array(0...UInt32((pointSize) * 2 - 3)) 18 | } else { 19 | indices = Array(0...((pointSize) * 2 - 3)).map { UInt32(($0 + 1) / 2) } 20 | } 21 | let inds = SCNGeometryElement(indices: indices, primitiveType: .line) 22 | return SCNGeometry(sources: [src], elements: [inds]) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/ViewController+ARSCNViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+ARSCNViewDelegate.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | 11 | extension ViewController: ARSCNViewDelegate { 12 | 13 | func session(_ session: ARSession, didFailWithError error: Error) { 14 | } 15 | 16 | func sessionWasInterrupted(_ session: ARSession) { 17 | } 18 | 19 | func sessionInterruptionEnded(_ session: ARSession) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/ViewController+Bezier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+Bezier.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | extension ViewController { 12 | 13 | func addPoints(count: Int) { 14 | self.bezierPoints = Array(0.. BezierPoint in 15 | return BezierPoint(at: SCNVector3(Float(index - (count / 2)) / 4, 0, -1)) 16 | } 17 | self.bezierPoints.forEach { (node) in 18 | self.bezierParent.addChildNode(node) 19 | } 20 | } 21 | 22 | func getLatestGeometry(count: Int = 100) -> SCNGeometry { 23 | return self.bezierPoints.getBezierGeometry(with: count) 24 | } 25 | 26 | @objc func runAnimation() { 27 | self.goButton.isHidden = true 28 | self.bezierParent.isHidden = true 29 | 30 | // Show statistics such as fps and timing information 31 | self.sceneView.showsStatistics = true 32 | 33 | let animNode = SCNNode(geometry: SCNSphere(radius: 0.05)) 34 | animNode.geometry?.firstMaterial?.diffuse.contents = UIColor.orange 35 | animNode.position = bezierPoints.first!.position 36 | self.sceneView.scene.rootNode.addChildNode(animNode) 37 | let bezierCurve = bezierPoints.getBezierPath() 38 | animNode.runAction(SCNAction.sequence([ 39 | SCNAction.moveAlong(path: bezierCurve, duration: 3), 40 | SCNAction.wait(duration: 1) 41 | ])) { 42 | animNode.removeFromParentNode() 43 | DispatchQueue.main.async { 44 | self.goButton.isHidden = false 45 | self.sceneView.showsStatistics = false 46 | } 47 | self.bezierParent.isHidden = false 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/ViewController+Gestures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+Gestures.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | private func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 12 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 13 | } 14 | 15 | extension ViewController: UIGestureRecognizerDelegate { 16 | func setupGestures() { 17 | // Pan gesture 18 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.translateObject(_:))) 19 | panGesture.delegate = self 20 | self.view.addGestureRecognizer(panGesture) 21 | cameraFrameNode.isHidden = true 22 | } 23 | 24 | @objc func translateObject(_ gestureRecognizer: UIGestureRecognizer) { 25 | if gestureRecognizer.state == .began { 26 | let location: CGPoint = gestureRecognizer.location(in: self.sceneView) 27 | let hits = self.sceneView.hitTest(location, options: [.boundingBoxOnly: true, .rootNode: self.bezierParent] 28 | ) 29 | if let firstHit = hits.first(where: { $0.node is BezierPoint}), 30 | let node = firstHit.node as? BezierPoint { 31 | self.hitNode = node 32 | lastParent = node.parent 33 | let cameraCoords = node.parent!.convertPosition( 34 | node.position, to: self.sceneView.pointOfView 35 | ) 36 | startPos = cameraCoords 37 | self.sceneView.pointOfView?.addChildNode(cameraFrameNode) 38 | cameraFrameNode.position.z = cameraCoords.z 39 | cameraFrameNode.eulerAngles.x = -.pi / 2 40 | cameraFrameNode.opacity = 0.1 41 | self.sceneView.pointOfView?.addChildNode(node) 42 | node.position = cameraCoords 43 | } 44 | } else if self.hitNode == nil || lastParent == nil { 45 | return 46 | } else if gestureRecognizer.state == .changed, 47 | let panGesture = gestureRecognizer as? UIPanGestureRecognizer { 48 | let lastPoint = panGesture.location(in: self.view) 49 | let nextPoint = lastPoint + panGesture.translation(in: self.view) 50 | guard let lastHit = self.sceneView.hitTest(lastPoint, options: [ 51 | SCNHitTestOption.rootNode: cameraFrameNode, SCNHitTestOption.ignoreHiddenNodes: false 52 | ]).first, let currentHit = self.sceneView.hitTest(nextPoint, options: [ 53 | SCNHitTestOption.rootNode: cameraFrameNode, SCNHitTestOption.ignoreHiddenNodes: false 54 | ]).first else { 55 | return 56 | } 57 | self.hitNode?.position.x = startPos!.x + (currentHit.localCoordinates.x - lastHit.localCoordinates.x) 58 | self.hitNode?.position.y = startPos!.y + (currentHit.localCoordinates.z - lastHit.localCoordinates.z) 59 | } else if gestureRecognizer.state == .ended, let hitNode = self.hitNode { 60 | let worldCoords = sceneView.pointOfView!.convertPosition(hitNode.position, to: lastParent) 61 | lastParent?.addChildNode(hitNode) 62 | hitNode.position = worldCoords 63 | self.hitNode = nil 64 | lastParent = nil 65 | self.bezierGeometryNode.geometry = self.getLatestGeometry() 66 | self.bezierGeometryNode.geometry?.firstMaterial?.diffuse.contents = UIColor.green 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/ViewController+UI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+UI.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension ViewController { 12 | func makeGoButton() { 13 | let frameSize = self.view.bounds.size 14 | let safeEdges = self.view.safeAreaInsets 15 | let newButton = UIButton(frame: CGRect( 16 | x: frameSize.width * 0.1, 17 | y: frameSize.height - (frameSize.width * 0.8 * 0.3) - safeEdges.bottom, 18 | width: frameSize.width * 0.8, 19 | height: frameSize.width * 0.8 * 0.3 20 | )) 21 | newButton.backgroundColor = .orange 22 | newButton.setTitle("run action", for: .normal) 23 | newButton.setTitleColor(UIColor.red, for: .normal) 24 | 25 | // runAnimation defined in ViewController+SCNBezier 26 | newButton.addTarget(self, action: #selector(runAnimation), for: .touchUpInside) 27 | 28 | self.goButton = newButton 29 | self.view.addSubview(newButton) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Example/SCNBezier-Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SCNBezier-Example 4 | // 5 | // Created by Max Cobb on 7/6/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var sceneView = ARSCNView(frame: .zero) 14 | var bezierPoints: [BezierPoint] = [] 15 | let bezierParent = SCNNode() 16 | var goButton: UIButton! = nil 17 | let bezierGeometryNode = SCNNode() 18 | 19 | // MARK: Gesture Variables 20 | var lastParent: SCNNode? 21 | var hitNode: BezierPoint? 22 | var startPos: SCNVector3? 23 | var cameraFrameNode = SCNNode(geometry: SCNFloor()) 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | self.sceneView.frame = self.view.bounds 29 | self.view.addSubview(self.sceneView) 30 | 31 | // Set the view's delegate 32 | self.sceneView.delegate = self 33 | 34 | self.setupGestures() 35 | self.makeGoButton() 36 | self.addPoints(count: 4) 37 | self.sceneView.scene.rootNode.addChildNode(self.bezierGeometryNode) 38 | self.sceneView.scene.rootNode.addChildNode(self.bezierParent) 39 | } 40 | 41 | override func viewWillAppear(_ animated: Bool) { 42 | super.viewWillAppear(animated) 43 | 44 | // Create a session configuration 45 | let configuration = ARWorldTrackingConfiguration() 46 | 47 | // Run the view's session 48 | sceneView.session.run(configuration) 49 | } 50 | 51 | override func viewWillDisappear(_ animated: Bool) { 52 | super.viewWillDisappear(animated) 53 | 54 | // Pause the view's session 55 | sceneView.session.pause() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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.0 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: "SCNBezier", 8 | platforms: [.iOS(.v9), .macOS(.v10_10), .tvOS(.v9), .watchOS(.v3)], 9 | products: [.library(name: "SCNBezier", targets: ["SCNBezier"])], 10 | targets: [ 11 | .target(name: "SCNBezier"), 12 | .testTarget( 13 | name: "SCNBezierTests", 14 | dependencies: ["SCNBezier"]) 15 | ], 16 | swiftLanguageVersions: [.v5] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SceneKit-Bezier-Animations 2 | 3 | Animate a SCNNode along a curved path in SceneKit. 4 | 5 | 6 | [![Linter Status](https://github.com/maxxfrazer/SceneKit-Bezier-Animations/workflows/linter/badge.svg)](https://github.com/maxxfrazer/SceneKit-Bezier-Animations/actions) 7 | [![Build Status](https://github.com/maxxfrazer/SceneKit-Bezier-Animations/workflows/build/badge.svg)](https://github.com/maxxfrazer/SceneKit-Bezier-Animations/actions) 8 | 9 | [![License](https://img.shields.io/github/license/maxxfrazer/SceneKit-Bezier-Animations?color=lightgray)](https://github.com/maxxfrazer/SceneKit-Bezier-Animations/blob/master/LICENSE) 10 | [![Platform](https://img.shields.io/cocoapods/p/SCNBezier)](https://cocoapods.org/pods/SCNBezier) 11 | [![SwiftPM](https://img.shields.io/github/v/release/maxxfrazer/SceneKit-Bezier-Animations?color=orange&include_prereleases)](https://github.com/apple/swift-package-manager) 12 | [![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg)](https://swift.org/) 13 | 14 | -------- 15 | ## Installation 16 | 17 | ### Swift Package Manager 18 | Add the URL of this repository to _Xcode 11_ with minimum version 1.3.0 19 | 20 | ### Cocoapods 21 | 22 | Add the following to your Podfile 23 | `pod 'SCNBezier'` 24 | 25 | -------- 26 | ## Usage 27 | 28 | ### Animating 29 | 30 | Using this framework you can animate a Node along a bézier path using the following example code: 31 | 32 | ``` 33 | let myNode = SCNNode(geometry: SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)) 34 | let bezPositions = [ 35 | SCNVector3(-1, 1, 0.01), 36 | SCNVector3(1, 0.5, 0.4), 37 | SCNVector3(1.0, -1, 0.1), 38 | SCNVector3(0.4, -0.5, 0.01) 39 | ] 40 | myNode.runAction(SCNAction.moveAlong(points: bezPositions, duration: 3, fps: 12)) 41 | ``` 42 | 43 | The animation will use `duration * fps` points to animated between; this requires type Double, or its TypeAlias TimeInterval. 44 | This does not mean it will not be smooth, it just means it will make the curve out of that many points, so in this example it will use 36 points along the Bézier curve. 45 | 46 | ### Getting a Bézier curve 47 | 48 | The class `SCNBezierPath` can be created if you want to collect n points along a path for positioning objects etc. as such: 49 | 50 | ``` 51 | let path = SCNBezierPath(points: points) 52 | for point in path.getNPoints(count: 20) { 53 | let newNode = SCNNode(geometry: nodeGeometry) 54 | newNode.position = point 55 | parent.addChildNode(newNode) 56 | } 57 | ``` 58 | 59 | ### Position on a Bézier curve 60 | 61 | If you want to get a point some percentage along the curve, there's a function that makes that easy for you too. 62 | ``` 63 | let path = SCNBezierPath(points: points) 64 | let quaterWay = path.posAt(time: 0.25) // gets the point 25% along from start to end 65 | ``` 66 | 67 | With points, nodeGeometry and parent being arbitrary values. 68 | 69 | -------- 70 | 71 | ![Bézier example](https://github.com/maxxfrazer/SceneKit-Bezier-Animations/blob/master/media/bezier-example.gif) 72 | -------------------------------------------------------------------------------- /SCNBezier.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 3 | s.name = 'SCNBezier' 4 | s.version = '1.3.1' 5 | s.summary = 'SCNBezier allows users to animate objects along 3D bézier curves.' 6 | s.description = <<-DESC 7 | SCNBezier allows users to animate objects along bézier curves. 8 | Any number of points can be used for the bézier curves, not limited to just quadratic or cubic 9 | DESC 10 | s.social_media_url = 'https://twitter.com/maxxfrazer' 11 | s.homepage = 'https://github.com/maxxfrazer/SceneKit-Bezier-Animations' 12 | s.license = 'MIT' 13 | s.author = 'Max Cobb' 14 | s.source = { :git => 'https://github.com/maxxfrazer/SceneKit-Bezier-Animations.git', :tag => "#{s.version}" } 15 | 16 | s.ios.deployment_target = '8.0' 17 | s.osx.deployment_target = '10.10' 18 | s.watchos.deployment_target = '3.0' 19 | s.tvos.deployment_target = '9.0' 20 | s.swift_version = '5.0' 21 | s.frameworks = 'SceneKit' 22 | 23 | s.source_files = 'Sources/SCNBezier/*.swift' 24 | end 25 | -------------------------------------------------------------------------------- /Sources/SCNBezier/InterpolatorFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterpolatorFunctions.swift 3 | // SCNBezier 4 | // 5 | // Created by Max Cobb on 11/10/2018. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class InterpolatorFunctions { 12 | public static func bounceOut(tIn: TimeInterval) -> TimeInterval { 13 | var tFloat = min(max(tIn, 0), 1) 14 | if tFloat < (1/2.75) { 15 | return 7.5625*tFloat*tFloat 16 | } else if tFloat < (2/2.75) { 17 | tFloat -= 1.5/2.75 18 | return 7.5625*tFloat*tFloat + 0.75 19 | } else if tFloat < (2.5/2.75) { 20 | tFloat -= (2.25/2.75) 21 | return 7.5625*tFloat*tFloat + 0.9375 22 | } else { 23 | tFloat -= 2.625/2.75 24 | return 7.5625*tFloat*tFloat + 0.984375 25 | } 26 | } 27 | 28 | public static func easeInExpo(tIn: TimeInterval) -> TimeInterval { 29 | let tClamped = min(max(tIn, 0), 1) 30 | return tClamped == 0 ? 0 : pow(2, 10 * (tClamped - 1)) 31 | } 32 | public static func easeOutExpo(tIn: TimeInterval) -> TimeInterval { 33 | let tClamped = min(max(tIn, 0), 1) 34 | return tClamped == 1 ? 1 : (1 - pow(2, -10 * tClamped)) 35 | } 36 | public static func easeInOutExpo(tIn: TimeInterval) -> TimeInterval { 37 | var tClamped = min(max(tIn, 0), 1) 38 | if tClamped==0 || tClamped == 1 { return tClamped } 39 | tClamped *= 2 40 | if tClamped < 1 { return 1/2 * pow(2, 10 * (tClamped - 1)) } 41 | return 1/2 * (2 - pow(2, -10 * (tClamped - 1))) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SCNBezier/SCNAction+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCNAction+Extensions.swift 3 | // SCNBezier 4 | // 5 | // Created by Max Cobb on 08/10/2018. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | public extension SCNAction { 12 | /// Move along a SCNBezierPath 13 | /// 14 | /// - Parameters: 15 | /// - path: SCNBezierPath to animate along 16 | /// - duration: time to travel the entire path 17 | /// - fps: how frequent the position should be updated (default 30) 18 | /// - interpolator: time interpolator for easing 19 | /// - Returns: SCNAction to be applied to a node 20 | static func moveAlong( 21 | path: SCNBezierPath, duration: TimeInterval, fps: Int = 30, 22 | interpolator: ((TimeInterval) -> TimeInterval)? = nil 23 | ) -> SCNAction { 24 | let actions = SCNAction.getActions( 25 | path: path, duration: duration, fps: fps, 26 | interpolator: interpolator 27 | ) 28 | return SCNAction.sequence(actions) 29 | } 30 | 31 | internal static func getActions( 32 | path: SCNBezierPath, duration: TimeInterval, fps: Int = 30, 33 | interpolator: ((TimeInterval) -> TimeInterval)? = nil 34 | ) -> [SCNAction] { 35 | let nPoints = path.getNPoints( 36 | count: max(2, Int(ceil(duration * Double(fps)))), interpolator: interpolator 37 | ) 38 | let actions = nPoints.enumerated().map { (iterator) -> SCNAction in 39 | if iterator.offset == 0 { 40 | // The first action should be instant, making sure the 41 | // SCNNode is in the starting position 42 | return SCNAction.move(to: iterator.element, duration: 0) 43 | } 44 | // The duration of each actuion should be a fraction of the full duration 45 | // n points, n - 1 moving actions, so duration / (n - 1) 46 | let tInt = duration / Double(nPoints.count - 1) 47 | return SCNAction.move(to: iterator.element, duration: tInt) 48 | } 49 | return actions 50 | } 51 | 52 | /// Move along a Bezier Path represented by a list of SCNVector3 53 | /// 54 | /// - Parameters: 55 | /// - path: List of points to for m Bezier Path to animate along 56 | /// - duration: time to travel the entire path 57 | /// - fps: how frequent the position should be updated (default 30) 58 | /// - interpolator: time interpolator for easing (see InterpolatorFunctions) 59 | /// - Returns: SCNAction to be applied to a node 60 | class func moveAlong( 61 | bezier path: [SCNVector3], duration: TimeInterval, 62 | fps: Int = 30, interpolator: ((TimeInterval) -> TimeInterval)? = nil 63 | ) -> SCNAction { 64 | return SCNAction.moveAlong( 65 | path: SCNBezierPath(points: path), 66 | duration: duration, fps: fps, 67 | interpolator: interpolator 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/SCNBezier/SCNBezierPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCNBezierPath.swift 3 | // SCNBezier 4 | // 5 | // Created by Max Cobb on 08/10/2018. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | public class SCNBezierPath { 12 | private let points: [SCNVector3] 13 | public init(points: [SCNVector3]) { 14 | self.points = points 15 | } 16 | /// Get position along bezier curve at a given time 17 | /// 18 | /// - Parameter time: Time as a percentage along bezier curve 19 | /// - Returns: position on the bezier curve 20 | public func posAt(time: TimeInterval) -> SCNVector3 { 21 | guard let first = self.points.first, let last = self.points.last else { 22 | print("NO POINTS IN SCNBezierPath") 23 | return SCNVector3Zero 24 | } 25 | if time == 0 { 26 | return first 27 | } else if time == 1 { 28 | return last 29 | } 30 | 31 | #if os(macOS) 32 | let tFloat = CGFloat(time) 33 | #else 34 | let tFloat = Float(time) 35 | #endif 36 | 37 | var high = self.points.count 38 | var current = 0 39 | var rtn = self.points 40 | while high > 0 { 41 | while current < high - 1 { 42 | rtn[current] = rtn[current] * (1 - tFloat) + rtn[current + 1] * tFloat 43 | current += 1 44 | } 45 | high -= 1 46 | current = 0 47 | } 48 | return rtn.first! 49 | } 50 | /// Collection of points evenly separated along the bezier curve from beginning to end 51 | /// 52 | /// - Parameters: 53 | /// - count: how many points you want 54 | /// - interpolator: time interpolator for easing 55 | /// - Returns: array of "count" points the points on the bezier curve 56 | public func getNPoints( 57 | count: Int, interpolator: ((TimeInterval) -> TimeInterval)? = nil 58 | ) -> [SCNVector3] { 59 | var bezPoints: [SCNVector3] = Array(repeating: SCNVector3Zero, count: count) 60 | for time in 0.. SCNVector3 { 18 | return SCNVector3Make(left.x - right.x, left.y - right.y, left.z - right.z) 19 | } 20 | internal func + (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 21 | return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z) 22 | } 23 | internal func * (left: SCNVector3, right: VectorVal) -> SCNVector3 { 24 | return SCNVector3Make(left.x * right, left.y * right, left.z * right) 25 | } 26 | -------------------------------------------------------------------------------- /Tests/SCNBezierTests/SCNBezierTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SCNBezier 3 | import SceneKit 4 | 5 | internal func - (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 6 | return SCNVector3Make(left.x - right.x, left.y - right.y, left.z - right.z) 7 | } 8 | internal func + (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 9 | return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z) 10 | } 11 | internal func * (left: SCNVector3, right: VectorVal) -> SCNVector3 { 12 | return SCNVector3Make(left.x * right, left.y * right, left.z * right) 13 | } 14 | 15 | internal extension SCNVector3 { 16 | var lengthSquared: Float { 17 | Float(sqrt(x * x + y * y + z * z)) 18 | } 19 | } 20 | 21 | final class SCNBezierTests: XCTestCase { 22 | func testBasicBezier() throws { 23 | let bezPositions = [ 24 | SCNVector3(-1, 1, 0.01), 25 | SCNVector3(1, 0.5, 0.4), 26 | SCNVector3(1.0, -1, 0.1), 27 | SCNVector3(0.4, -0.5, 0.01) 28 | ] 29 | 30 | let points = SCNBezierPath(points: bezPositions).getNPoints(count: 100) 31 | XCTAssertTrue(points.count == 100, "Wrong number of points: \(bezPositions.count)") 32 | checkPositionsEqual(bezPositions.first!, points.first!) 33 | checkPositionsEqual(bezPositions.last!, points.last!) 34 | } 35 | 36 | func testUnevenValue() throws { 37 | let bezPositions = [ 38 | SCNVector3(-1, 1, 0.01), 39 | SCNVector3(1, 0.5, 0.4), 40 | SCNVector3(1.0, -1, 0.1), 41 | SCNVector3(0.4, -0.5, 0.01) 42 | ] 43 | let bezPath = SCNBezierPath(points: bezPositions) 44 | let actions = SCNAction.getActions(path: bezPath, duration: 0.3, fps: 1) 45 | let actionSequence = SCNAction.sequence(actions) 46 | XCTAssertTrue(actions.count == 2, "should have at least 2 actions!") 47 | XCTAssertTrue(actionSequence.duration == 0.3, "Action sequence wrong length: \(actionSequence.duration)") 48 | XCTAssertTrue(actions.first!.duration == 0, "Action sequence wrong length: \(actions.first!.duration)") 49 | XCTAssertTrue(actions.last!.duration == 0.3, "Action sequence wrong length: \(actions.last!.duration)") 50 | } 51 | 52 | func checkPositionsEqual(_ first: SCNVector3, _ second: SCNVector3, prependMessage: String = "") { 53 | let endDiff = (first - second).lengthSquared 54 | XCTAssertTrue(endDiff < 1e-5, "\(prependMessage)\nLast point is not correct \(first) vs \(second)") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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.34.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 24 | -------------------------------------------------------------------------------- /media/bezier-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxxfrazer/SceneKit-Bezier-Animations/b928e191bdb73770f7c09909955800f5e2ccf9c0/media/bezier-example.gif -------------------------------------------------------------------------------- /old.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | before_install: 4 | - gem install cocoapods 5 | install: 6 | - ./install_swiftlint.sh 7 | script: 8 | - gem install travis --no-document 9 | - travis lint .travis.yml --no-interactive 10 | - swiftlint 11 | - swift package generate-xcodeproj 12 | - xcodebuild clean build -project SCNBezier.xcodeproj -scheme SCNBezier-Package -destination "platform=iOS Simulator,name=iPhone Xs" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO 13 | - xcodebuild clean build -project SCNBezier.xcodeproj -scheme SCNBezier-Package -destination "platform=macOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO 14 | - pod lib lint --allow-warnings 15 | --------------------------------------------------------------------------------