Skip to content

re-enable spring sleeping #428

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/Animation/ExternalTime.luau
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class.type = "State"
class.kind = "ExternalTime"
class.timeliness = "lazy"
class.dependencySet = table.freeze {}
class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep()

local METATABLE = table.freeze {__index = class}

Expand All @@ -40,8 +39,9 @@ local function ExternalTime(
dependentSet = {},
lastChange = nil,
scope = scope,
validity = "invalid"
},
validity = "invalid",
_EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep(),
},
METATABLE
) :: any
local destroy = function()
Expand All @@ -66,13 +66,13 @@ function class._evaluate(
-- the external update step runs, it's safe enough to assume that the result
-- has always meaningfully changed. The worst that can happen is unexpected
-- refreshing for people doing unorthodox shenanigans, which is an OK trade.
self._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep()
return true
end

External.bindToUpdateStep(function(
externalNow: number
): ()
class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep()
for _, timer in allTimers do
change(timer)
end
Expand Down
37 changes: 16 additions & 21 deletions src/Animation/Spring.luau
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ local function Spring<T>(
self.oldestTask = destroy
nicknames[self.oldestTask] = "Spring"
table.insert(scope, destroy)

if goalState ~= nil then
checkLifetime.bOutlivesA(
scope, self.oldestTask,
Expand Down Expand Up @@ -203,8 +203,12 @@ function class._evaluate<T>(
self._EXTREMELY_DANGEROUS_usedAsValue = self._goal :: T
return false
end
-- depend(self, goal)
local nextFrameGoal = peek(goal)

local nextFrameGoal = depend(self, goal)

local stopwatch = self._stopwatch :: Stopwatch.Stopwatch
local elapsed = depend(self, stopwatch)

-- Protect against NaN goals.
if nextFrameGoal ~= nextFrameGoal then
External.logWarn("springNanGoal")
Expand All @@ -213,28 +217,21 @@ function class._evaluate<T>(
local nextFrameGoalType = typeof(nextFrameGoal)
local discontinuous = nextFrameGoalType ~= self._activeType

local stopwatch = self._stopwatch :: Stopwatch.Stopwatch
local elapsed = peek(stopwatch)
depend(self, stopwatch)

local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
local newValue: T

if discontinuous then
-- Propagate changes in type instantly throughout the whole reactive
-- graph, even if simulation is logically one frame behind, because it
-- makes the whole graph behave more consistently.
newValue = nextFrameGoal
elseif elapsed <= 0 then
newValue = oldValue
else
-- Calculate spring motion.
-- IMPORTANT: use the parameters from last frame, not this frame. We're
-- integrating the motion that happened over the last frame, after all.
-- The stopwatch will have captured the length of time needed correctly.
local posPos, posVel, velPos, velVel = springCoefficients(
elapsed,
self._activeDamping,
elapsed,
self._activeDamping,
self._activeSpeed
)
local isMoving = false
Expand All @@ -257,14 +254,11 @@ function class._evaluate<T>(
self._activeLatestV[index] = latestV
end
-- Sleep and snap to goal if the motion has decayed to a negligible amount.
if not isMoving then
for index = 1, self._activeNumSprings do
self._activeLatestP[index] = self._activeTargetP[index]
end
-- TODO: figure out how to do sleeping correctly for single frame
-- changes
-- stopwatch:pause()
-- stopwatch:zero()
if not isMoving and stopwatch:isPlaying() then
self._activeLatestP = table.clone(self._activeTargetP)
self._activeLatestV = table.create(self._activeNumSprings, 0)
stopwatch:pause()
stopwatch:zero()
end
-- Pack springs into final value.
newValue = packType(self._activeLatestP, self._activeType) :: any
Expand Down Expand Up @@ -305,9 +299,10 @@ function class._evaluate<T>(
-- Don't need to use the similarity test here because this code doesn't
-- deal with tables, and NaN is already guarded against, so the similarity
-- test doesn't actually add any new safety here.
local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
self._EXTREMELY_DANGEROUS_usedAsValue = newValue
return oldValue ~= newValue
end

table.freeze(class)
return Spring :: Types.SpringConstructor
return Spring :: Types.SpringConstructor
36 changes: 23 additions & 13 deletions src/Animation/Stopwatch.luau
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ local nicknames = require(Package.Utility.nicknames)
export type Stopwatch = Types.StateObject<number> & {
zero: (Stopwatch) -> (),
pause: (Stopwatch) -> (),
unpause: (Stopwatch) -> ()
unpause: (Stopwatch) -> (),
isPlaying: (Stopwatch) -> boolean,
}

type Self = Stopwatch & {
_measureTimeSince: number,
_playing: boolean,
_zeroFlag: boolean,
_timer: Types.StateObject<number>
}

Expand Down Expand Up @@ -58,8 +60,9 @@ local function Stopwatch(
_EXTREMELY_DANGEROUS_usedAsValue = 0,
_measureTimeSince = 0, -- this should be set on unpause
_playing = false,
_timer = timer
},
_zeroFlag = true,
_timer = timer,
},
METATABLE
) :: any
local destroy = function()
Expand All @@ -81,12 +84,9 @@ end
function class.zero(
self: Self
): ()
local newTimepoint = peek(self._timer)
if newTimepoint ~= self._measureTimeSince then
self._measureTimeSince = newTimepoint
self._EXTREMELY_DANGEROUS_usedAsValue = 0
change(self)
end
self._zeroFlag = true
self._measureTimeSince = peek(self._timer)
change(self)
end

function class.pause(
Expand All @@ -103,25 +103,35 @@ function class.unpause(
): ()
if self._playing == false then
self._playing = true
self._measureTimeSince = peek(self._timer) - self._EXTREMELY_DANGEROUS_usedAsValue
self._measureTimeSince = peek(self._timer) - (
if self._zeroFlag then
0
else
self._EXTREMELY_DANGEROUS_usedAsValue
)
change(self)
end
end

function class.isPlaying(
self: Self
): boolean
return self._playing
end

function class._evaluate(
self: Self
): boolean
if self._playing then
depend(self, self._timer)
local currentTime = peek(self._timer)
self._zeroFlag = false
local currentTime = depend(self, self._timer)
local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
local newValue = currentTime - self._measureTimeSince
self._EXTREMELY_DANGEROUS_usedAsValue = newValue
return oldValue ~= newValue
else
return false
end

end

table.freeze(class)
Expand Down
2 changes: 0 additions & 2 deletions src/Graph/change.luau
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ local function change(
done = false
table.insert(invalidateList, dependent)
table.insert(searchInNext, dependent)
elseif dependent.validity == "busy" then
return External.logError("infiniteLoop")
end
end
end
Expand Down
38 changes: 25 additions & 13 deletions src/Graph/depend.luau
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,35 @@ local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
local evaluate = require(Package.Graph.evaluate)
local castToGraph = require(Package.Graph.castToGraph)
local castToState = require(Package.State.castToState)
local nameOf = require(Package.Utility.nameOf)

local function depend<T>(
local depend = function<T>(
dependent: Types.GraphObject,
dependency: Types.GraphObject
): ()
-- Ensure dependencies are evaluated and up-to-date
-- when they are depended on. Also, newly created objects
-- might not have any transitive dependencies captured yet,
-- so ensure that they're present.
evaluate(dependency, false)
dependency: Types.UsedAs<T> | Types.GraphObject
): any
if castToGraph(dependency) then
local dependency = dependency :: Types.GraphObject
-- Ensure dependencies are evaluated and up-to-date
-- when they are depended on. Also, newly created objects
-- might not have any transitive dependencies captured yet,
-- so ensure that they're present.
evaluate(dependency, false)

if table.isfrozen(dependent.dependencySet) or table.isfrozen(dependency.dependentSet) then
External.logError("cannotDepend", nil, nameOf(dependent, "Dependent"), nameOf(dependency, "dependency"))
if table.isfrozen(dependent.dependencySet) or table.isfrozen(dependency.dependentSet) then
External.logError("cannotDepend", nil, nameOf(dependent, "Dependent"), nameOf(dependency, "dependency"))
end
dependency.dependentSet[dependent] = true
dependent.dependencySet[dependency] = true

if castToState(dependency) then
return (dependency :: Types.StateObject<T>)._EXTREMELY_DANGEROUS_usedAsValue :: T
else
return nil
end
end
dependency.dependentSet[dependent] = true
dependent.dependencySet[dependency] = true
end
return dependency :: T
end :: (<T>(Types.GraphObject, Types.UsedAs<T>) -> T) & ((Types.GraphObject, Types.GraphObject) -> ())

return depend