mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Update spring solver
This commit is contained in:
parent
fdc72a17f0
commit
8ef2d0b190
5 changed files with 280 additions and 120 deletions
|
|
@ -29,7 +29,7 @@ Returns a new source with a dynamically animated value of the input source.
|
||||||
|
|
||||||
`damping_ratio` is the amount of resistance applied to the spring.
|
`damping_ratio` is the amount of resistance applied to the spring.
|
||||||
|
|
||||||
- \>1 = Overdamped (not currently supported).
|
- \>1 = Overdamped - slowly reaches target without any overshoot.
|
||||||
- 1 = Critically damped - reaches target without any overshoot.
|
- 1 = Critically damped - reaches target without any overshoot.
|
||||||
- <1 = Underdamped - reaches target with some overshoot.
|
- <1 = Underdamped - reaches target with some overshoot.
|
||||||
- 0 = Undamped - never stabilizes, oscillates forever.
|
- 0 = Undamped - never stabilizes, oscillates forever.
|
||||||
|
|
|
||||||
285
src/spring.luau
285
src/spring.luau
|
|
@ -1,4 +1,5 @@
|
||||||
if not game then script = require "test/relative-string" end
|
if not game then script = require "test/relative-string" end
|
||||||
|
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
|
||||||
|
|
@ -10,88 +11,122 @@ Supported datatypes:
|
||||||
- UDim2
|
- UDim2
|
||||||
- Vector2
|
- Vector2
|
||||||
- Vector3
|
- Vector3
|
||||||
|
- Rect
|
||||||
|
|
||||||
Unsupported datatypes:
|
Unsupported datatypes:
|
||||||
- bool
|
- bool
|
||||||
- Rect
|
|
||||||
- Vector2int16
|
- Vector2int16
|
||||||
- Vector3int16
|
- Vector3int16
|
||||||
- EnumItem
|
- EnumItem
|
||||||
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
|
||||||
local graph = require(script.Parent.graph)
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local create = graph.create
|
local create = graph.create
|
||||||
local get = graph.get
|
|
||||||
local set = graph.set
|
local set = graph.set
|
||||||
local set_effect = graph.set_effect
|
local set_effect = graph.set_effect
|
||||||
local capture = graph.capture
|
local capture = graph.capture
|
||||||
|
|
||||||
local TOLERANCE = 0.0001
|
local UPDATE_RATE = 120
|
||||||
|
local TOLERANCE = 0.00001
|
||||||
|
|
||||||
|
type Vec3 = Vector3
|
||||||
|
|
||||||
|
local function Vec3(x: number?, y: number?, z: number?)
|
||||||
|
return Vector3.new(x, y, z)
|
||||||
|
end
|
||||||
|
|
||||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||||
|
|
||||||
type SpringData<T> = {
|
type SpringData<T> = {
|
||||||
time: number,
|
k: number, -- spring constant
|
||||||
period: number,
|
c: number, -- damping coeff (from k)
|
||||||
damping_ratio: number,
|
|
||||||
|
|
||||||
alpha: number,
|
-- dimensions 1-3
|
||||||
velocity: number,
|
x0_123: Vec3,
|
||||||
initial_velocity: number,
|
x1_123: Vec3,
|
||||||
initial_position: T,
|
v_123: Vec3,
|
||||||
target_position: T,
|
|
||||||
|
|
||||||
target_updated: boolean,
|
-- dimensions 4-6
|
||||||
target: () -> T
|
x0_456: Vec3,
|
||||||
|
x1_456: Vec3,
|
||||||
|
v_456: Vec3,
|
||||||
|
|
||||||
|
source_value: T
|
||||||
}
|
}
|
||||||
|
|
||||||
type Lerp<T> = (initial: T, target: T, alpha: number) -> T
|
type TypeToVec6<T> = (T) -> (Vec3, Vec3)
|
||||||
|
type Vec6ToType<T> = (Vec3, Vec3) -> T
|
||||||
|
|
||||||
-- period, damping ratio, initial velocity, total time
|
local type_to_vec6 = {
|
||||||
local function solve(T: number, z: number, u: number, t: number): number -- alpha
|
number = function(v)
|
||||||
local wn = 2*math.pi / T
|
return Vec3(v, 0, 0), Vec3()
|
||||||
local wd = wn * math.sqrt(1 - z^2)
|
end :: TypeToVec6<number>,
|
||||||
local a = z * wn
|
|
||||||
|
|
||||||
local s = math.exp(-a*t) * math.cos(wd*t)
|
CFrame = function(v)
|
||||||
local v = (u/wn) * math.exp(-a*t) * math.sin(wn*t)
|
-- todo: proper rotation tween
|
||||||
return (1-s) + v
|
return v.Position, Vec3(v:ToEulerAnglesXYZ())
|
||||||
end
|
end :: TypeToVec6<CFrame>,
|
||||||
|
|
||||||
local lerpable: { [string]: Lerp<any> } = {
|
Color3 = function(v)
|
||||||
number = function(v1, v2, a)
|
-- todo: hsv
|
||||||
return v1 + (v2 - v1)*a
|
return Vec3(v.R, v.G, v.B), Vec3()
|
||||||
end :: Lerp<number>,
|
end :: TypeToVec6<Color3>,
|
||||||
|
|
||||||
CFrame = function(v1, v2, a)
|
UDim = function(v)
|
||||||
return v1:Lerp(v2, a)
|
return Vec3(v.Scale, v.Offset, 0), Vec3()
|
||||||
end :: Lerp<CFrame>,
|
end :: TypeToVec6<UDim>,
|
||||||
|
|
||||||
|
UDim2 = function(v)
|
||||||
|
return Vec3(v.X.Scale, v.X.Offset, v.Y.Scale), Vec3(v.Y.Offset, 0, 0)
|
||||||
|
end :: TypeToVec6<UDim2>,
|
||||||
|
|
||||||
Color3 = function(v1, v2, a)
|
Vector2 = function(v)
|
||||||
return v1:Lerp(v2, a)
|
return Vec3(v.X, v.Y, 0), Vec3()
|
||||||
end :: Lerp<Color3>,
|
end :: TypeToVec6<Vector2>,
|
||||||
|
|
||||||
UDim = function(v1, v2, a)
|
Vector3 = function(v)
|
||||||
return UDim.new(
|
return v, Vec3()
|
||||||
v1.Scale + (v2.Scale - v1.Scale)*a,
|
end :: TypeToVec6<Vector3>,
|
||||||
v1.Offset + (v2.Offset - v1.Offset)*a
|
|
||||||
)
|
|
||||||
end :: Lerp<UDim>,
|
|
||||||
|
|
||||||
UDim2 = function(v1, v2, a)
|
Rect = function(v)
|
||||||
return v1:Lerp(v2, a)
|
return Vec3(v.Min.X, v.Min.Y, v.Max.X), Vec3(v.Max.Y, 0, 0)
|
||||||
end :: Lerp<UDim2>,
|
end :: TypeToVec6<Rect>
|
||||||
|
}
|
||||||
|
|
||||||
Vector2 = function(v1, v2, a)
|
local vec6_to_type = {
|
||||||
return v1:Lerp(v2, a)
|
number = function(a, b)
|
||||||
end :: Lerp<Vector2>,
|
return a.X
|
||||||
|
end :: Vec6ToType<number>,
|
||||||
|
|
||||||
Vector3 = function(v1, v2, a)
|
CFrame = function(a, b)
|
||||||
return v1:Lerp(v2, a)
|
return CFrame.new(a) * CFrame.fromEulerAnglesXYZ(b.X, b.Y, b.Z)
|
||||||
end :: Lerp<Vector3>,
|
end :: Vec6ToType<CFrame>,
|
||||||
|
|
||||||
|
Color3 = function(v)
|
||||||
|
return Color3.new(v.X, v.Y, v.Z)
|
||||||
|
end :: Vec6ToType<Color3>,
|
||||||
|
|
||||||
|
UDim = function(v)
|
||||||
|
return UDim.new(v.X, v.Y)
|
||||||
|
end :: Vec6ToType<UDim>,
|
||||||
|
|
||||||
|
UDim2 = function(a, b)
|
||||||
|
return UDim2.new(a.X, a.Y, a.Z, b.X)
|
||||||
|
end :: Vec6ToType<UDim2>,
|
||||||
|
|
||||||
|
Vector2 = function(v)
|
||||||
|
return Vector2.new(v.X, v.Y)
|
||||||
|
end :: Vec6ToType<Vector2>,
|
||||||
|
|
||||||
|
Vector3 = function(v)
|
||||||
|
return v
|
||||||
|
end :: Vec6ToType<Vector3>,
|
||||||
|
|
||||||
|
Rect = function(a, b)
|
||||||
|
return Rect.new(a.X, a.Y, a.Z, b.X)
|
||||||
|
end :: Vec6ToType<Rect>
|
||||||
}
|
}
|
||||||
|
|
||||||
-- maps spring data to its corresponding output node
|
-- maps spring data to its corresponding output node
|
||||||
|
|
@ -99,33 +134,46 @@ local lerpable: { [string]: Lerp<any> } = {
|
||||||
local springs: { [SpringData<any>]: Node<any> } = {}
|
local springs: { [SpringData<any>]: Node<any> } = {}
|
||||||
setmetatable(springs, { __mode = "vs" })
|
setmetatable(springs, { __mode = "vs" })
|
||||||
|
|
||||||
local function spring<T>(target: () -> T, period: number?, damping_ratio: number?): () -> T
|
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||||
if damping_ratio and damping_ratio > 1 then
|
local inputs, initial_position = capture(source)
|
||||||
throw "damping ratio cannot be greater than 1"
|
|
||||||
end
|
|
||||||
|
|
||||||
local inputs, initial_position = capture(target)
|
|
||||||
local output, output_get = create(initial_position)
|
local output, output_get = create(initial_position)
|
||||||
|
|
||||||
|
local source_value = source()
|
||||||
|
local vtype = typeof(source_value)
|
||||||
|
|
||||||
|
local x1_123, x1_456 = type_to_vec6[vtype](source_value)
|
||||||
|
|
||||||
|
-- https://en.wikipedia.org/wiki/Damping
|
||||||
|
-- todo: calculate damped freq at 10tau instead of natural freq
|
||||||
|
|
||||||
|
local w_n = 2*math.pi / (period or 1)
|
||||||
|
local z = damping_ratio or 1
|
||||||
|
|
||||||
|
local k = w_n^2
|
||||||
|
local c_c = 2*w_n
|
||||||
|
local c = z * c_c
|
||||||
|
|
||||||
local data: SpringData<T> = {
|
local data: SpringData<T> = {
|
||||||
time = 0,
|
k = k,
|
||||||
period = period or 1,
|
c = c,
|
||||||
damping_ratio = damping_ratio or 1,
|
|
||||||
|
|
||||||
alpha = 0,
|
x0_123 = x1_123,
|
||||||
velocity = 0,
|
x1_123 = x1_123,
|
||||||
initial_velocity = 0,
|
v_123 = Vec3(),
|
||||||
initial_position = initial_position,
|
|
||||||
target_position = initial_position,
|
|
||||||
|
|
||||||
target_updated = false,
|
x0_456 = x1_456,
|
||||||
target = target
|
x1_456 = x1_456,
|
||||||
|
v_456 = Vec3(),
|
||||||
|
|
||||||
|
source_value = source_value,
|
||||||
|
_ = source -- prevent gc of source while data exists
|
||||||
}
|
}
|
||||||
|
|
||||||
-- reschedule spring for simulation on input update
|
-- reschedule spring for simulation on input update
|
||||||
local function input_updated(node)
|
local function input_updated(node)
|
||||||
data.target_updated = true
|
local v = source()
|
||||||
data.target_position = target()
|
data.x1_123, data.x1_456 = type_to_vec6[type(v)](v)
|
||||||
|
data.source_value = v
|
||||||
springs[data] = node
|
springs[data] = node
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -139,60 +187,54 @@ local function spring<T>(target: () -> T, period: number?, damping_ratio: number
|
||||||
return output_get
|
return output_get
|
||||||
end
|
end
|
||||||
|
|
||||||
-- `springs` is a hashmap, use array to queue indexes-to-remove to avoid
|
local function step_springs(dt: number)
|
||||||
-- iterator invalidation of `springs`
|
for data, output in next, springs do
|
||||||
|
local k, c, x0_123, x1_123, u_123, x0_456, x1_456, u_456 = data.k, data.c, data.x0_123, data.x1_123, data.v_123, data.x0_456, data.x1_456, data.v_456
|
||||||
|
|
||||||
|
-- calculate displacement from target
|
||||||
|
local dx_123 = x0_123 - x1_123
|
||||||
|
local dx_456 = x0_456 - x1_456
|
||||||
|
|
||||||
|
-- calculate spring force
|
||||||
|
local fs_123 = dx_123*-k
|
||||||
|
local fs_456 = dx_456*-k
|
||||||
|
|
||||||
|
-- calculate friction force
|
||||||
|
local ff_123 = u_123*-c
|
||||||
|
local ff_456 = u_456*-c
|
||||||
|
|
||||||
|
-- calculate acceleration step
|
||||||
|
local dv_123 = (fs_123 + ff_123)*dt
|
||||||
|
local dv_456 = (fs_456 + ff_456)*dt
|
||||||
|
|
||||||
|
-- apply acceleration step
|
||||||
|
local v_123 = u_123 + dv_123
|
||||||
|
local v_456 = u_456 + dv_456
|
||||||
|
|
||||||
|
-- calculate new position
|
||||||
|
local x_123 = x0_123 + v_123*dt
|
||||||
|
local x_456 = x0_456 + v_456*dt
|
||||||
|
|
||||||
|
data.x0_123, data.x0_456 = x_123, x_456
|
||||||
|
data.v_123, data.v_456 = v_123, v_456
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local remove_queue = {}
|
local remove_queue = {}
|
||||||
|
|
||||||
local function update_springs(dt: number)
|
local function update_spring_sources()
|
||||||
for data, output in next, springs do
|
for data, output in next, springs do
|
||||||
if data.target_updated then
|
local x0_123, x1_123, v_123, x0_456, x1_456, v_456 = data.x0_123, data.x1_123, data.v_123, data.x0_456, data.x1_456, data.v_456
|
||||||
data.target_updated = false
|
|
||||||
data.time = 0
|
local dx_123, dx_456 = x0_123 - x1_123, x0_456 - x1_456
|
||||||
data.alpha = 0
|
|
||||||
data.initial_velocity = data.velocity
|
|
||||||
data.initial_position = get(output)
|
|
||||||
data.target_position = data.target()
|
|
||||||
end
|
|
||||||
|
|
||||||
local initial_position = data.initial_position
|
-- todo: can this false positive?
|
||||||
local target_position = data.target_position
|
if (v_123 + v_456 + dx_123 + dx_456).Magnitude < TOLERANCE then
|
||||||
local target_type = typeof(target_position)
|
|
||||||
|
|
||||||
if target_type ~= typeof(initial_position) then
|
|
||||||
springs[data] = nil
|
|
||||||
warn(string.format(
|
|
||||||
"Mismatched state value types, cancelling state update (initial value: %s, target value: %s)",
|
|
||||||
typeof(initial_position),
|
|
||||||
target_type
|
|
||||||
))
|
|
||||||
throw(`cannot tween type { typeof(initial_position) } and { target_type }`)
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
local lerp: Lerp<Animatable> = lerpable[target_type]
|
|
||||||
|
|
||||||
if lerp == nil then
|
|
||||||
springs[data] = nil
|
|
||||||
throw(`cannot animate type { target_type }`)
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
local new_time = data.time + dt
|
|
||||||
local new_alpha = solve(data.period, data.damping_ratio, data.initial_velocity, new_time)
|
|
||||||
local new_velocity = -(new_alpha - data.alpha)/dt
|
|
||||||
|
|
||||||
data.time = new_time
|
|
||||||
data.velocity = new_velocity
|
|
||||||
data.alpha = new_alpha
|
|
||||||
|
|
||||||
local value = lerp(initial_position, target_position, new_alpha)
|
|
||||||
|
|
||||||
if math.abs(1 - new_alpha) < TOLERANCE and math.abs(new_velocity) < TOLERANCE then
|
|
||||||
-- close enough to target, unshedule spring and set value to target
|
-- close enough to target, unshedule spring and set value to target
|
||||||
table.insert(remove_queue, data)
|
table.insert(remove_queue, data)
|
||||||
set(output, target_position)
|
set(output, data.source_value)
|
||||||
else
|
else
|
||||||
set(output, value)
|
set(output, vec6_to_type[typeof(data.source_value)](x0_123, x0_456))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -203,4 +245,17 @@ local function update_springs(dt: number)
|
||||||
table.clear(remove_queue)
|
table.clear(remove_queue)
|
||||||
end
|
end
|
||||||
|
|
||||||
return function() return spring, update_springs end
|
return function()
|
||||||
|
local time_elapsed = 0
|
||||||
|
|
||||||
|
return spring, function(dt: number)
|
||||||
|
time_elapsed += dt
|
||||||
|
|
||||||
|
while time_elapsed > 1 / UPDATE_RATE do
|
||||||
|
time_elapsed -= 1 / UPDATE_RATE
|
||||||
|
step_springs(1 / UPDATE_RATE)
|
||||||
|
end
|
||||||
|
|
||||||
|
update_spring_sources()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,40 @@ local Vector2 = { __type = "Vector2" } :: any do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local Vector3 = { __type = "Vector3" } :: any do
|
||||||
|
local function new(x, y, z)
|
||||||
|
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.new(x, y, z)
|
||||||
|
return new(x or 0, y or 0, z or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__add(a, b)
|
||||||
|
return new(a.X + b.X, a.Y + b.Y, a.Z + b.Z)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__sub(a, b)
|
||||||
|
return new(a.X - b.X, a.Y - b.Y, a.Z - b.Z)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__mul(a, b)
|
||||||
|
return new(a.X * b, a.Y * b, a.Z * b)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__unm(v)
|
||||||
|
return new(-v.X, -v.Y, -v.Z)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__eq(a, b)
|
||||||
|
return a.X == b.X and a.Y == b.Y
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__index(v)
|
||||||
|
return (v.X^2 + v.Y^2 + v.Z^2)^0.5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local UDim2 = { __type = "UDim2" } :: any do
|
local UDim2 = { __type = "UDim2" } :: any do
|
||||||
function UDim2.new(sx, ox, sy, oy)
|
function UDim2.new(sx, ox, sy, oy)
|
||||||
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
||||||
|
|
@ -294,8 +328,8 @@ return {
|
||||||
Signal = Signal,
|
Signal = Signal,
|
||||||
Instance = Instance :: typeof(Instance),
|
Instance = Instance :: typeof(Instance),
|
||||||
Color3 = Color3 :: typeof(Color3),
|
Color3 = Color3 :: typeof(Color3),
|
||||||
Vector3 = Vector3 :: typeof(Vector3),
|
|
||||||
Vector2 = Vector2 :: typeof(Vector2),
|
Vector2 = Vector2 :: typeof(Vector2),
|
||||||
|
Vector3 = Vector3 :: typeof(Vector3),
|
||||||
UDim2 = UDim2 :: typeof(UDim2),
|
UDim2 = UDim2 :: typeof(UDim2),
|
||||||
Enum = Enum :: typeof(Enum),
|
Enum = Enum :: typeof(Enum),
|
||||||
typeof = typeof :: typeof(typeof)
|
typeof = typeof :: typeof(typeof)
|
||||||
|
|
|
||||||
71
test/spring-test.luau
Normal file
71
test/spring-test.luau
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
local vide = require "src/init"
|
||||||
|
local testkit = require("test/testkit")
|
||||||
|
|
||||||
|
local program_time = os.clock()
|
||||||
|
|
||||||
|
local function step(): number
|
||||||
|
local FPS = 60
|
||||||
|
local DT = 1/FPS
|
||||||
|
|
||||||
|
repeat until os.clock() - program_time >= DT
|
||||||
|
program_time += DT
|
||||||
|
return DT
|
||||||
|
end
|
||||||
|
|
||||||
|
local function main()
|
||||||
|
local source = vide.source
|
||||||
|
local spring = vide.spring
|
||||||
|
local watch = vide.watch
|
||||||
|
|
||||||
|
local TERMINAL_HEIGHT = 73 --* REDUCE IF BAR DOES NOT FIT IN TERMINAL
|
||||||
|
local MIN_ALPHA = 0.3
|
||||||
|
local MAX_ALPHA = 0.7
|
||||||
|
|
||||||
|
local MIN = TERMINAL_HEIGHT * MIN_ALPHA
|
||||||
|
local MAX = TERMINAL_HEIGHT * MAX_ALPHA
|
||||||
|
local OFFSET = TERMINAL_HEIGHT - MAX
|
||||||
|
|
||||||
|
local BLOCK = "█"
|
||||||
|
|
||||||
|
local function remainder_to_block(x)
|
||||||
|
return
|
||||||
|
if x > 7/8 then "█"
|
||||||
|
elseif x > 6/8 then "▇"
|
||||||
|
elseif x > 5/8 then "▆"
|
||||||
|
elseif x > 4/8 then "▅"
|
||||||
|
elseif x > 3/8 then "▄"
|
||||||
|
elseif x > 2/8 then "▃"
|
||||||
|
elseif x > 1/8 then "▂"
|
||||||
|
else "▁"
|
||||||
|
end
|
||||||
|
|
||||||
|
local value = source(MAX)
|
||||||
|
local sprung = spring(value, 1, 0.3)
|
||||||
|
|
||||||
|
watch(function()
|
||||||
|
local v = sprung()
|
||||||
|
local fv = math.floor(v)
|
||||||
|
local reset = "\27[H\27[2J" -- ANSI clear terminal
|
||||||
|
local offset = string.rep("\n", MAX - fv + OFFSET)
|
||||||
|
local bar = testkit.color.gray(remainder_to_block(v - fv) .. "\n" .. string.rep(BLOCK .. "\n", fv))
|
||||||
|
print(reset .. offset .. bar)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local elapsed = 0
|
||||||
|
repeat local dt = step()
|
||||||
|
vide.step(dt)
|
||||||
|
|
||||||
|
local T = 3
|
||||||
|
elapsed += dt
|
||||||
|
while elapsed >= T do
|
||||||
|
elapsed -= T
|
||||||
|
value(value() == MAX and MIN or MAX)
|
||||||
|
end
|
||||||
|
|
||||||
|
until false
|
||||||
|
end
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1240,7 +1240,7 @@ TEST("spring()", function()
|
||||||
input(1)
|
input(1)
|
||||||
vide.step(0.05)
|
vide.step(0.05)
|
||||||
CHECK(output() ~= input()) -- check spring is moving
|
CHECK(output() ~= input()) -- check spring is moving
|
||||||
vide.step(6) -- spring finished, should be internally removed from queue
|
vide.step(10) -- spring finished, should be internally removed from queue
|
||||||
CHECK(output() == input()) -- check spring is at target
|
CHECK(output() == input()) -- check spring is at target
|
||||||
|
|
||||||
local count = -1
|
local count = -1
|
||||||
|
|
@ -1250,13 +1250,13 @@ TEST("spring()", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
vide.step(1) -- attempt to cause another spring update
|
vide.step(1) -- attempt to cause another spring update
|
||||||
CHECK(count == 1) -- check no update occurs as spring is finished
|
CHECK(count == 0) -- check no update occurs as spring is finished
|
||||||
--
|
--
|
||||||
|
|
||||||
gc() -- perform full gc
|
gc() -- perform full gc
|
||||||
input(2) -- spring should be re-added to spring queue
|
input(2) -- spring should be re-added to spring queue
|
||||||
vide.step(0) -- process spring queue
|
vide.step(0) -- process spring queue
|
||||||
CHECK(count == 2) -- check spring was rescheduled correctly
|
CHECK(count == 1) -- check spring was rescheduled correctly
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue