diff --git a/src/spring.luau b/src/spring.luau index ef7decf..63a308f 100644 --- a/src/spring.luau +++ b/src/spring.luau @@ -10,6 +10,7 @@ local push_scope_as_child_of = graph.push_scope_as_child_of local UPDATE_RATE = 120 local TOLERANCE_FACTOR = 10_000 +local MIN_TOLERANCE = vector.create(1e-6, 1e-6, 1e-6) type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 @@ -170,6 +171,7 @@ local function spring(source: () -> T, period: number?, damping_ratio: number local function updater_effect() local value = source() + data.x0_123, data.x0_456 = data.x_123, data.x_456 data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value) data.source_value = value springs[data] = output @@ -219,6 +221,7 @@ local function spring(source: () -> T, period: number?, damping_ratio: number -- set current position to value local v = ... :: T data.x_123, data.x_456 = type_to_vec6[typeof(v)](v) + data.x0_123, data.x0_456 = data.x_123, data.x_456 -- reset velocity data.v_123 = vector.zero @@ -246,6 +249,12 @@ local function get_min_vector_step(direction: vector): vector ) end +-- relative to the distance travelled, with a floor so springs can still settle at zero and large goals +local function get_tolerance(x0: vector, x1: vector): vector + local distance = vector.max(vector.abs(x0 - x1), vector.abs(x1)) + return vector.max(get_min_vector_step(distance), MIN_TOLERANCE) +end + local function step_springs(dt: number) for s in springs do local k = s.k @@ -290,8 +299,8 @@ local function update_spring_sources() local x1_123, x1_456 = data.x1_123, data.x1_456 local v_123, v_456 = data.v_123, data.v_456 - local tol_123 = vector.abs(get_min_vector_step(x0_123 - x1_123)) - local tol_456 = vector.abs(get_min_vector_step(x0_456 - x1_456)) + local tol_123 = get_tolerance(x0_123, x1_123) + local tol_456 = get_tolerance(x0_456, x1_456) if -- position is at goal (within tolerance) diff --git a/test/tests.luau b/test/tests.luau index af6a7f4..cd9cd1d 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -2121,6 +2121,32 @@ TEST("spring()", wrap_root(function() CHECK(count == 1) -- check spring was rescheduled correctly end + do CASE "spring finished at zero" + local input = source(1) + local output, control = spring(input) + + local count = 0 + effect(function() + output() + count += 1 + end) + + input(0) + step(10) + CHECK(output() == 0) + + count = 0 + step(1) + CHECK(count == 0) -- check spring is finished + + control { impulse = 1 } + step(10) + + count = 0 + step(1) + CHECK(count == 0) -- check spring is finished after impulse at rest + end + do CASE "spring control" local input = source(1) local output, control = spring(input)