diff --git a/.github/workflows/swift-build.yml b/.github/workflows/swift-build.yml index 099bc1f..9416546 100644 --- a/.github/workflows/swift-build.yml +++ b/.github/workflows/swift-build.yml @@ -2,24 +2,18 @@ name: build on: push: - branches: - - "master" - tags: - - "!*" + branches: [ master ] pull_request: - branches: - - "*" + branches: [ master ] jobs: build: - runs-on: macOS-latest + + runs-on: macos-latest + steps: - - uses: actions/checkout@v1 - - name: Build Package - run: | - swift package generate-xcodeproj - xcodebuild clean build -project $PROJECT -scheme $SCHEME -destination "$DESTINATION" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO - env: - PROJECT: SCNBezier.xcodeproj - SCHEME: SCNBezier-Package - DESTINATION: platform=iOS Simulator,name=iPhone Xs + - uses: actions/checkout@v2 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v diff --git a/Package.swift b/Package.swift index f207f5d..5740fe1 100644 --- a/Package.swift +++ b/Package.swift @@ -5,8 +5,13 @@ import PackageDescription let package = Package( name: "SCNBezier", - platforms: [.iOS(.v8), .macOS(.v10_10), .tvOS(.v9), .watchOS(.v3)], + platforms: [.iOS(.v9), .macOS(.v10_10), .tvOS(.v9), .watchOS(.v3)], products: [.library(name: "SCNBezier", targets: ["SCNBezier"])], - targets: [.target(name: "SCNBezier")], + targets: [ + .target(name: "SCNBezier"), + .testTarget( + name: "SCNBezierTests", + dependencies: ["SCNBezier"]) + ], swiftLanguageVersions: [.v5] ) diff --git a/Sources/SCNBezier/SCNAction+Extensions.swift b/Sources/SCNBezier/SCNAction+Extensions.swift index 71d037f..6867a71 100644 --- a/Sources/SCNBezier/SCNAction+Extensions.swift +++ b/Sources/SCNBezier/SCNAction+Extensions.swift @@ -17,17 +17,38 @@ public extension SCNAction { /// - fps: how frequent the position should be updated (default 30) /// - interpolator: time interpolator for easing /// - Returns: SCNAction to be applied to a node - class func moveAlong( + static func moveAlong( path: SCNBezierPath, duration: TimeInterval, fps: Int = 30, interpolator: ((TimeInterval) -> TimeInterval)? = nil ) -> SCNAction { - let actions = path.getNPoints(count: Int(duration) * fps, interpolator: interpolator).map { (point) -> SCNAction in - let tInt = 1 / TimeInterval(fps) - return SCNAction.move(to: point, duration: tInt) - } + let actions = SCNAction.getActions( + path: path, duration: duration, fps: fps, + interpolator: interpolator + ) return SCNAction.sequence(actions) } + internal static func getActions( + path: SCNBezierPath, duration: TimeInterval, fps: Int = 30, + interpolator: ((TimeInterval) -> TimeInterval)? = nil + ) -> [SCNAction] { + let nPoints = path.getNPoints( + count: max(2, Int(ceil(duration * Double(fps)))), interpolator: interpolator + ) + let actions = nPoints.enumerated().map { (iterator) -> SCNAction in + if iterator.offset == 0 { + // The first action should be instant, making sure the + // SCNNode is in the starting position + return SCNAction.move(to: iterator.element, duration: 0) + } + // The duration of each actuion should be a fraction of the full duration + // n points, n - 1 moving actions, so duration / (n - 1) + let tInt = duration / Double(nPoints.count - 1) + return SCNAction.move(to: iterator.element, duration: tInt) + } + return actions + } + /// Move along a Bezier Path represented by a list of SCNVector3 /// /// - Parameters: diff --git a/Tests/SCNBezierTests/SCNBezierTests.swift b/Tests/SCNBezierTests/SCNBezierTests.swift new file mode 100644 index 0000000..19974da --- /dev/null +++ b/Tests/SCNBezierTests/SCNBezierTests.swift @@ -0,0 +1,57 @@ + +import XCTest +@testable import SCNBezier +import SceneKit + +internal func - (left: SCNVector3, right: SCNVector3) -> SCNVector3 { + return SCNVector3Make(left.x - right.x, left.y - right.y, left.z - right.z) +} +internal func + (left: SCNVector3, right: SCNVector3) -> SCNVector3 { + return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z) +} +internal func * (left: SCNVector3, right: VectorVal) -> SCNVector3 { + return SCNVector3Make(left.x * right, left.y * right, left.z * right) +} + +internal extension SCNVector3 { + var length_squared: Float { + Float(sqrt(x * x + y * y + z * z)) + } +} + +final class SCNBezierTests: XCTestCase { + func testBasicBezier() throws { + let bezPositions = [ + SCNVector3(-1, 1, 0.01), + SCNVector3(1, 0.5, 0.4), + SCNVector3(1.0, -1, 0.1), + SCNVector3(0.4, -0.5, 0.01) + ] + + let points = SCNBezierPath(points: bezPositions).getNPoints(count: 100) + XCTAssertTrue(points.count == 100, "Wrong number of points: \(bezPositions.count)") + checkPositionsEqual(bezPositions.first!, points.first!) + checkPositionsEqual(bezPositions.last!, points.last!) + } + + func testUnevenValue() throws { + let bezPositions = [ + SCNVector3(-1, 1, 0.01), + SCNVector3(1, 0.5, 0.4), + SCNVector3(1.0, -1, 0.1), + SCNVector3(0.4, -0.5, 0.01) + ] + let bezPath = SCNBezierPath(points: bezPositions) + let actions = SCNAction.getActions(path: bezPath, duration: 0.3, fps: 1) + let actionSequence = SCNAction.sequence(actions) + XCTAssertTrue(actions.count == 2, "should have at least 2 actions!") + XCTAssertTrue(actionSequence.duration == 0.3, "Action sequence wrong length: \(actionSequence.duration)") + XCTAssertTrue(actions.first!.duration == 0, "Action sequence wrong length: \(actions.first!.duration)") + XCTAssertTrue(actions.last!.duration == 0.3, "Action sequence wrong length: \(actions.last!.duration)") + } + + func checkPositionsEqual(_ first: SCNVector3, _ second: SCNVector3, prependMessage: String = "") { + let endDiff = (first - second).length_squared + XCTAssertTrue(endDiff < 1e-5, "\(prependMessage)\nLast point is not correct \(first) vs \(second)") + } +}