Skip to content

Commit a37ea34

Browse files
committed
Added basic spring tests
1 parent b0d9af0 commit a37ea34

File tree

4 files changed

+110
-16
lines changed

4 files changed

+110
-16
lines changed

Advance.xcodeproj/project.pbxproj

+14-6
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@
9797
228A676C226F77CB0086A37C /* SimulationFunction+Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228A676B226F77CB0086A37C /* SimulationFunction+Integration.swift */; };
9898
228A676D226F77CB0086A37C /* SimulationFunction+Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228A676B226F77CB0086A37C /* SimulationFunction+Integration.swift */; };
9999
228A676E226F77CB0086A37C /* SimulationFunction+Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228A676B226F77CB0086A37C /* SimulationFunction+Integration.swift */; };
100+
228A677A226F7B3C0086A37C /* SpringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228A6779226F7B3C0086A37C /* SpringTests.swift */; };
101+
228A677B226F7B3C0086A37C /* SpringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228A6779226F7B3C0086A37C /* SpringTests.swift */; };
102+
228A677C226F7B3C0086A37C /* SpringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228A6779226F7B3C0086A37C /* SpringTests.swift */; };
100103
/* End PBXBuildFile section */
101104

102105
/* Begin PBXContainerItemProxy section */
@@ -213,6 +216,7 @@
213216
22881F35226AE9D900309FEF /* TimingFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimingFunction.swift; sourceTree = "<group>"; };
214217
228A6755226F72E40086A37C /* AnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatorTests.swift; sourceTree = "<group>"; };
215218
228A676B226F77CB0086A37C /* SimulationFunction+Integration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SimulationFunction+Integration.swift"; sourceTree = "<group>"; };
219+
228A6779226F7B3C0086A37C /* SpringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringTests.swift; sourceTree = "<group>"; };
216220
22A4664622063B0400C0FEA1 /* Advance.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Advance.framework; sourceTree = BUILT_PRODUCTS_DIR; };
217221
22A4665622063B4B00C0FEA1 /* Advance.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Advance.framework; sourceTree = BUILT_PRODUCTS_DIR; };
218222
22A4666322063B6400C0FEA1 /* Advance.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Advance.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -425,6 +429,7 @@
425429
22881F14226AE99D00309FEF /* AnimationTests.swift */,
426430
22881F15226AE99D00309FEF /* VectorConvenienceTests.swift */,
427431
228A6755226F72E40086A37C /* AnimatorTests.swift */,
432+
228A6779226F7B3C0086A37C /* SpringTests.swift */,
428433
);
429434
path = AdvanceTests;
430435
sourceTree = "<group>";
@@ -433,25 +438,25 @@
433438
isa = PBXGroup;
434439
children = (
435440
22881F2C226AE9D900309FEF /* Internal */,
436-
22881F29226AE9D900309FEF /* Spring.swift */,
437-
22881F2A226AE9D900309FEF /* DecayFunction.swift */,
438-
22881F2B226AE9D900309FEF /* SpringFunction.swift */,
439441
22881F31226AE9D900309FEF /* Animator.swift */,
442+
22881F2A226AE9D900309FEF /* DecayFunction.swift */,
440443
22881F32226AE9D900309FEF /* SimulationFunction.swift */,
444+
22881F29226AE9D900309FEF /* Spring.swift */,
445+
22881F2B226AE9D900309FEF /* SpringFunction.swift */,
446+
22881F35226AE9D900309FEF /* TimingFunction.swift */,
441447
22881F33226AE9D900309FEF /* UnitBezier.swift */,
442448
22881F34226AE9D900309FEF /* VectorConvertible.swift */,
443-
22881F35226AE9D900309FEF /* TimingFunction.swift */,
444449
);
445450
path = Advance;
446451
sourceTree = "<group>";
447452
};
448453
22881F2C226AE9D900309FEF /* Internal */ = {
449454
isa = PBXGroup;
450455
children = (
451-
22881F2D226AE9D900309FEF /* Math.swift */,
452456
22881F2E226AE9D900309FEF /* Animation.swift */,
453-
22881F2F226AE9D900309FEF /* Simulation.swift */,
454457
22881F30226AE9D900309FEF /* DisplayLink.swift */,
458+
22881F2D226AE9D900309FEF /* Math.swift */,
459+
22881F2F226AE9D900309FEF /* Simulation.swift */,
455460
228A676B226F77CB0086A37C /* SimulationFunction+Integration.swift */,
456461
);
457462
path = Internal;
@@ -878,6 +883,7 @@
878883
files = (
879884
22881F25226AE99D00309FEF /* VectorConvenienceTests.swift in Sources */,
880885
22881F1F226AE99D00309FEF /* DisplayLinkTests.swift in Sources */,
886+
228A677A226F7B3C0086A37C /* SpringTests.swift in Sources */,
881887
228A6756226F72E40086A37C /* AnimatorTests.swift in Sources */,
882888
22881F19226AE99D00309FEF /* UnitBezierTests.swift in Sources */,
883889
22881F22226AE99D00309FEF /* AnimationTests.swift in Sources */,
@@ -892,6 +898,7 @@
892898
files = (
893899
22881F26226AE99D00309FEF /* VectorConvenienceTests.swift in Sources */,
894900
22881F20226AE99D00309FEF /* DisplayLinkTests.swift in Sources */,
901+
228A677B226F7B3C0086A37C /* SpringTests.swift in Sources */,
895902
228A6757226F72E40086A37C /* AnimatorTests.swift in Sources */,
896903
22881F1A226AE99D00309FEF /* UnitBezierTests.swift in Sources */,
897904
22881F23226AE99D00309FEF /* AnimationTests.swift in Sources */,
@@ -906,6 +913,7 @@
906913
files = (
907914
22881F27226AE99D00309FEF /* VectorConvenienceTests.swift in Sources */,
908915
22881F21226AE99D00309FEF /* DisplayLinkTests.swift in Sources */,
916+
228A677C226F7B3C0086A37C /* SpringTests.swift in Sources */,
909917
228A6758226F72E40086A37C /* AnimatorTests.swift in Sources */,
910918
22881F1B226AE99D00309FEF /* UnitBezierTests.swift in Sources */,
911919
22881F24226AE99D00309FEF /* AnimationTests.swift in Sources */,

Sources/Advance/Internal/Simulation.swift

+6-10
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@
1212
/// precise calculations.
1313
struct Simulation<Value: VectorConvertible> {
1414

15-
// The internal time step. 0.008 == 120fps (double the typical screen refresh
16-
// rate). The math required to solve most functions is easy for modern
17-
// CPUs, but it's worth experimenting with this value if solver calculations
18-
// ever become a performance bottleneck.
19-
fileprivate let tickTime: Double = 0.008
15+
2016

2117
/// The function driving the simulation.
2218
private var function: AnySimulationFunction<Value> {
@@ -81,7 +77,7 @@ struct Simulation<Value: VectorConvertible> {
8177
guard hasConverged == false else { return }
8278

8379
// Limit to 10 physics ticks per update, should never come close.
84-
let t = min(time, tickTime * 10.0)
80+
let t = min(time, simulationFrameDuration * 10.0)
8581

8682
// Add the new time to the accumulator. This can be thought of as the
8783
// delta between the time of the current physics state, and the time
@@ -98,12 +94,12 @@ struct Simulation<Value: VectorConvertible> {
9894
break
9995
}
10096
previous = current
101-
current = function.integrate(value: current.value, velocity: current.velocity, time: tickTime)
102-
timeAccumulator -= tickTime
97+
current = function.integrate(value: current.value, velocity: current.velocity, time: simulationFrameDuration)
98+
timeAccumulator -= simulationFrameDuration
10399
}
104100

105101
assert(timeAccumulator <= 0.0)
106-
assert(timeAccumulator > -tickTime)
102+
assert(timeAccumulator > -simulationFrameDuration)
107103

108104
// If convergence is possible, we can just do that and avoid interpolation.
109105
convergeIfPossible()
@@ -115,7 +111,7 @@ struct Simulation<Value: VectorConvertible> {
115111
// `previousState` and `simulationState`, and interpolate. This
116112
// will let us provide a more accurate value to the outside world,
117113
// while maintaining a consistent time step internally.
118-
let alpha = Double((tickTime + timeAccumulator) / tickTime)
114+
let alpha = Double((simulationFrameDuration + timeAccumulator) / simulationFrameDuration)
119115
interpolated.value = interpolate(from: previous.value, to: current.value, alpha: alpha)
120116
interpolated.velocity = interpolate(from: previous.velocity, to: current.velocity, alpha: alpha)
121117
}

Sources/Advance/Internal/SimulationFunction+Integration.swift

+46
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
// The internal time step. 0.008 == 120fps (double the typical screen refresh
2+
// rate). The math required to solve most functions is easy for modern
3+
// CPUs, but it's worth experimenting with this value if solver calculations
4+
// ever become a performance bottleneck.
5+
let simulationFrameDuration: Double = 0.008
6+
7+
18
/// [The RK4 method](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods)
29
/// is used to integrate the acceleration function.
310
extension SimulationFunction {
@@ -41,3 +48,42 @@ extension SimulationFunction {
4148
}
4249

4350
}
51+
52+
extension SpringFunction {
53+
54+
/// Estimates the value that the simulation will ultimately converge at, and the time that is required to reach convergence.
55+
///
56+
/// - Parameter initialValue: The initial value of the simulation
57+
/// - Parameter initialVelocity: The initial velocity of the simulation
58+
/// - Parameter maximumDuration: The maximum simulation time to calculate. Some simulation functions may never converge, so we need
59+
/// a way to cap the search.
60+
///
61+
/// - Returns: The converged value and the time required to reach convergence (if successful), or nil if convergence was not reached
62+
/// within the given `maximumDuration`.
63+
func estimatedConvergence(initialValue: Value, initialVelocity: Value, maximumDuration: Double) -> (value: Value, duration: Double)? {
64+
65+
var value = initialValue.vector
66+
var velocity = initialVelocity.vector
67+
var duration: Double = 0.0
68+
var hasConverged: Bool = false
69+
70+
while !hasConverged {
71+
(value, velocity) = integrate(value: value, velocity: velocity, time: simulationFrameDuration)
72+
duration += simulationFrameDuration
73+
switch convergence(value: value, velocity: velocity) {
74+
case .keepRunning:
75+
continue
76+
case .converge(atValue: let convergedValue):
77+
value = convergedValue
78+
hasConverged = true
79+
}
80+
81+
if duration > maximumDuration {
82+
return nil
83+
}
84+
}
85+
86+
return (value: Value(vector: value), duration: duration)
87+
}
88+
89+
}

Tests/AdvanceTests/SpringTests.swift

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import XCTest
2+
@testable import Advance
3+
4+
5+
class SpringTests: XCTestCase {
6+
7+
8+
func testReachesTargetValue() {
9+
10+
let spring = Spring(initialValue: 0.0)
11+
spring.tension = 500.0
12+
spring.damping = 40.0
13+
spring.threshold = 0.01
14+
15+
let toValue = 10.0
16+
17+
let finalValueExpectation = expectation(description: "Spring reaches the correct value")
18+
19+
spring.onChange = { value in
20+
if value == toValue {
21+
finalValueExpectation.fulfill()
22+
}
23+
}
24+
25+
let estimatedConvergence = SpringFunction(
26+
target: toValue,
27+
tension: spring.tension,
28+
damping: spring.damping,
29+
threshold: spring.threshold)
30+
.estimatedConvergence(
31+
initialValue: 0.0,
32+
initialVelocity: 10.0,
33+
maximumDuration: 10.0)!
34+
35+
spring.target = toValue
36+
37+
wait(for: [finalValueExpectation], timeout: estimatedConvergence.duration)
38+
}
39+
40+
static var allTests = [
41+
("testReachesTargetValue", testReachesTargetValue),
42+
]
43+
44+
}

0 commit comments

Comments
 (0)