mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Refactor codebase
This commit is contained in:
parent
07ae542e28
commit
a3f03fd067
15 changed files with 228 additions and 128 deletions
|
|
@ -1,13 +1,7 @@
|
|||
local typeof = typeof
|
||||
local Vector2 = Vector2
|
||||
local UDim2 = UDim2
|
||||
|
||||
if not game then
|
||||
script = require "test/relative-string"
|
||||
typeof = require "test/mock".typeof
|
||||
Vector2 = require "test/mock".Vector2
|
||||
UDim2 = require "test/mock".UDim2
|
||||
end
|
||||
if not game then script = require "test/relative-string" end
|
||||
local typeof = game and typeof or require "test/mock".typeof :: never
|
||||
local Vector2 = game and Vector2 or require "test/mock".Vector2 :: never
|
||||
local UDim2 = game and UDim2 or require "test/mock".UDim2 :: never
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
|
|
@ -16,9 +10,11 @@ local _, is_action = require(script.Parent.action)()
|
|||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
|
||||
-- buffer of event -> callback to connect after properties are set
|
||||
local event_buffer: { [string]: () -> () } = {}
|
||||
local action_buffers = {} :: { { () -> () } }
|
||||
|
||||
-- buffer of priority -> callback to run after events are connected
|
||||
local action_buffers = {} :: { { () -> () } }
|
||||
setmetatable(action_buffers :: any, {
|
||||
__index = function(_, i: number)
|
||||
action_buffers[i] = {}
|
||||
|
|
@ -26,8 +22,8 @@ setmetatable(action_buffers :: any, {
|
|||
end
|
||||
})
|
||||
|
||||
-- cache used in strict mode to detect duplicate property sets at same nesting levels
|
||||
local nested_debug_cache: { [number]: { [string]: true } } = {}
|
||||
|
||||
setmetatable(nested_debug_cache :: any, {
|
||||
__index = function(_, i: number)
|
||||
nested_debug_cache[i] = {}
|
||||
|
|
@ -35,10 +31,15 @@ setmetatable(nested_debug_cache :: any, {
|
|||
end
|
||||
})
|
||||
|
||||
-- a stack used in place of a recursive function to process nesting layers one at a time
|
||||
-- enforces the behavior of deeper-nested properties taking precedence of lesser-nested ones
|
||||
-- each nested table occupies two indexes, reference to table itself and the depth number
|
||||
-- e.g. props = { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||
local nested_stack = {} :: { {} | number }
|
||||
|
||||
-- todo
|
||||
local classes = {
|
||||
-- todo: solution without manual updating of this table
|
||||
-- map of datatype names to class default constructor for aggregate initialization
|
||||
local aggregates = {
|
||||
Vector2 = Vector2,
|
||||
UDim2 = UDim2,
|
||||
UDim = UDim2,
|
||||
|
|
@ -46,11 +47,12 @@ local classes = {
|
|||
Color3 = Color3
|
||||
}
|
||||
|
||||
local function get_class(v: unknown): { new: (...any) -> () }
|
||||
return classes[typeof(v)]
|
||||
for i, v in next, aggregates do
|
||||
aggregates[i] = v.new
|
||||
end
|
||||
|
||||
local function process(instance: Instance, properties: { [unknown]: unknown })
|
||||
-- processes a potentially nested table of values to assign to an instance
|
||||
local function process_nested(instance: Instance, properties: { [unknown]: unknown })
|
||||
local strict = flags.strict
|
||||
|
||||
table.clear(nested_stack)
|
||||
|
|
@ -61,71 +63,79 @@ local function process(instance: Instance, properties: { [unknown]: unknown })
|
|||
repeat
|
||||
for property, value in properties do
|
||||
if type(property) == "string" then
|
||||
if strict then
|
||||
if strict then -- check for duplicate prop assignment at nesting layer
|
||||
if nested_debug_cache[depth][property] then
|
||||
throw(`duplicate property {property} at depth {depth}`)
|
||||
end
|
||||
nested_debug_cache[depth][property] = true
|
||||
end
|
||||
|
||||
if type(value) == "table" then
|
||||
local class = get_class((instance :: any)[property])
|
||||
if class == nil then
|
||||
if type(value) == "table" then -- attempt aggregate init
|
||||
local ctor = aggregates[typeof((instance :: any)[property])]
|
||||
if ctor == nil then
|
||||
throw(`cannot aggregate construct type {typeof(value)} for property {property}`)
|
||||
end
|
||||
(instance :: any)[property] = class.new(unpack(value :: {}))
|
||||
(instance :: any)[property] = ctor(unpack(value :: {}))
|
||||
elseif type(value) == "function" then
|
||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||
event_buffer[property] = value :: () -> ()
|
||||
event_buffer[property] = value :: () -> () -- add event to buffer
|
||||
else
|
||||
bind.property(instance, property, value :: () -> ())
|
||||
bind.property(instance, property, value :: () -> ()) -- bind source
|
||||
end
|
||||
else
|
||||
(instance :: any)[property] = value
|
||||
(instance :: any)[property] = value -- set property
|
||||
end
|
||||
elseif type(property) == "number" then
|
||||
if type(value) == "function" then
|
||||
bind.children(instance, value :: () -> { Instance })
|
||||
bind.children(instance, value :: () -> { Instance }) -- bind children
|
||||
elseif type(value) == "table" then
|
||||
if is_action(value) then
|
||||
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ())
|
||||
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
else
|
||||
table.insert(nested_stack, depth + 1)
|
||||
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
|
||||
table.insert(nested_stack, value :: {})
|
||||
end
|
||||
else
|
||||
(value :: Instance).Parent = instance
|
||||
(value :: Instance).Parent = instance -- parent child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- pop next nested table off stack
|
||||
properties = table.remove(nested_stack) :: {}
|
||||
depth = table.remove(nested_stack) :: number
|
||||
|
||||
until not properties
|
||||
end
|
||||
|
||||
-- applies table of nested properties to an instance using full vide semantics
|
||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||
-- queue parent assignment if any for last
|
||||
local parent: unknown = properties.Parent
|
||||
if parent then properties.Parent = nil end
|
||||
|
||||
-- reset buffers
|
||||
table.clear(event_buffer)
|
||||
for _, buffer in next, action_buffers do
|
||||
table.clear(buffer)
|
||||
end
|
||||
|
||||
process(instance, properties)
|
||||
-- process all properties for immediate setting or buffering
|
||||
process_nested(instance, properties)
|
||||
|
||||
-- connect buffered events
|
||||
for event, fn in next, event_buffer do
|
||||
(instance :: any)[event]:Connect(fn)
|
||||
end
|
||||
|
||||
-- run buffered actions respecting their priorities
|
||||
for _, buffer in next, action_buffers do
|
||||
for _, callback in next, buffer do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
-- finally set parent if any
|
||||
if parent then
|
||||
if type(parent) == "function" then
|
||||
error("cannot set parent to state")
|
||||
|
|
|
|||
|
|
@ -1,22 +1,41 @@
|
|||
local warn = warn
|
||||
|
||||
if not game then
|
||||
script = require "test/relative-string"
|
||||
warn = print
|
||||
end
|
||||
if not game then script = require "test/relative-string" end
|
||||
local warn = game and warn or print :: never
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local set_effect = graph.set_effect
|
||||
local capture = graph.capture
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
--[[
|
||||
|
||||
Roblox instances in Luau are referenced using a kind of userdata proxy,
|
||||
this proxy can be garbage collected independently from the actual instance, even
|
||||
if the instance is still parented. Since reactive bindings allow the garbage
|
||||
collection of instances, this proxy can can garbage collected while the instance
|
||||
is still parented, causing the binding to be lost and no longer update the
|
||||
instance on changes.
|
||||
|
||||
Vide's solution to this is to hold the proxy in memory as long as the instance
|
||||
is parented to the datamodel by using `GetPropertyChanged("Parent")` to add or
|
||||
remove the proxy from a table whose sole purpose is to strongly reference
|
||||
proxies.
|
||||
|
||||
todo: investigate behavior in case B is parented to A, and A has no parent or reference, and B has a binding.
|
||||
|
||||
]]
|
||||
|
||||
-- holds parented instance proxies in memory
|
||||
local hold: { Instance? } = {}
|
||||
local weak: { Instance? } = setmetatable({}, { __mode = "v" }) :: any
|
||||
local bindcount = 0
|
||||
|
||||
-- weakly references instances with properties bound
|
||||
local weak: { Instance? } = setmetatable({}, { __mode = "v" }) :: any
|
||||
|
||||
-- unique binding id
|
||||
local bind_count = 0
|
||||
|
||||
-- todo: replace with throw's method
|
||||
local root do
|
||||
local src = debug.info(1, "s")
|
||||
root = string.sub(src, 1, #src - 5)
|
||||
|
|
@ -39,33 +58,40 @@ local function traceback(skips: number) -- ensures trace begins outside of any v
|
|||
return debug.traceback(nil, s)
|
||||
end
|
||||
|
||||
function setup(instance: Instance, debug_msg: string, setter: (Instance) -> ())
|
||||
function bind(instance: Instance, property: string, setter: (Instance) -> ())
|
||||
if flags.strict then
|
||||
-- wrap setter in function with stack inspection for better error msgs
|
||||
local fn = setter
|
||||
local bind_trace = traceback(0)
|
||||
setter = function(instance)
|
||||
local ok, err: string? = xpcall(fn, function(err: string)
|
||||
return err .. "\nsource updated at: " .. traceback(2)
|
||||
end, instance)
|
||||
if not ok then warn(`error occured updating {debug_msg}: {err}bound at: {bind_trace}`) end
|
||||
if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end
|
||||
end
|
||||
end
|
||||
|
||||
-- run setter to capture any nodes being depended on
|
||||
local nodes = (capture(setter :: () -> unknown, instance))
|
||||
|
||||
-- register the setter as a side-effect of each node
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, setter, instance)
|
||||
end
|
||||
|
||||
bindcount += 1
|
||||
local key = bindcount
|
||||
-- get binding id
|
||||
bind_count += 1
|
||||
local bind_id = bind_count
|
||||
|
||||
weak[key] = instance
|
||||
-- store reference of instance proxy without preventing gc
|
||||
weak[bind_id] = instance
|
||||
|
||||
local function ref()
|
||||
local _ = setter
|
||||
local instance = weak[key] :: Instance
|
||||
hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented
|
||||
local _ = setter -- prevent gc of nodes being depended on
|
||||
local instance = weak[bind_id] :: Instance
|
||||
|
||||
-- keep proxy in memory if instance is still parented
|
||||
hold[bind_id] = instance.Parent and instance or nil
|
||||
end
|
||||
|
||||
ref()
|
||||
|
|
@ -73,7 +99,7 @@ function setup(instance: Instance, debug_msg: string, setter: (Instance) -> ())
|
|||
end
|
||||
|
||||
local function bind_property(instance: Instance, property: string, fn: () -> unknown)
|
||||
setup(instance, property, function(instance_weak: any)
|
||||
bind(instance, property, function(instance_weak: any)
|
||||
instance_weak[property] = fn()
|
||||
end)
|
||||
end
|
||||
|
|
@ -83,7 +109,7 @@ local function bind_parent(instance: Instance, fn: () -> Instance?)
|
|||
instance = nil :: any -- allow gc when destroyed
|
||||
end)
|
||||
|
||||
setup(instance, "Parent", function(instance)
|
||||
bind(instance, "Parent", function(instance)
|
||||
local _ = instance -- state will strongly reference instance when parent is bound
|
||||
instance.Parent = fn()
|
||||
end)
|
||||
|
|
@ -93,7 +119,7 @@ local function bind_children(parent: Instance, fn: () -> { Instance })
|
|||
local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update
|
||||
local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update
|
||||
|
||||
setup(parent, "Children", function(parent_weak)
|
||||
bind(parent, "Children", function(parent_weak)
|
||||
local new_childs = fn() -- all (and only) children that should be parented after this update
|
||||
if new_childs and type(new_childs) ~= "table" then
|
||||
throw(`Cannot parent instance of type { type(new_childs) } `)
|
||||
|
|
|
|||
|
|
@ -3,15 +3,41 @@ if not game then script = require "test/relative-string" end
|
|||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
|
||||
--[[
|
||||
|
||||
Cleanups associate a callback with an arbitrary value with an unknown lifetime.
|
||||
Anytime a new callback is registered with a value that already has one registered,
|
||||
the registered callback is ran and then replaced with the new one.
|
||||
|
||||
When the value is eventually garbage collected, Vide checks for callbacks
|
||||
without an associated value, which it will then run and clear, recycling its
|
||||
cleanup id.
|
||||
|
||||
By default the arbitrary value is the function object that calls `cleanup()`.
|
||||
There are exceptions such as with `indexes()` and `values()` where the
|
||||
arbitrary value is manually set to be the new source created instead of the
|
||||
caller, as the same caller can be used to create multiple new objects.
|
||||
|
||||
todo: remove need for ref to id maps?
|
||||
|
||||
]]
|
||||
|
||||
-- maps a ref to cleanup id
|
||||
local ref_to_id = {} :: { [string]: number }
|
||||
-- maps a cleanup id to a ref
|
||||
local id_to_ref = {} :: { [number]: string }
|
||||
-- array of all cleanup callbacks
|
||||
local cleanup_callbacks = {} :: { [number]: () -> () } -- always dense
|
||||
-- weak array of all cleanup lifetimes
|
||||
local cleanup_lifetime = {} :: { [number]: unknown } -- can be sparse
|
||||
setmetatable(cleanup_lifetime :: any, { __mode = "v" })
|
||||
|
||||
-- detects in strict mode when multiple cleanups are registered in the same scope
|
||||
local debug_caller_to_line = {} :: { [() -> ()]: number }
|
||||
setmetatable(debug_caller_to_line, { __mode = "k" })
|
||||
|
||||
-- when active, cleanup callbacks are not automatically registered but are
|
||||
-- added to an array for manual registering internally
|
||||
local manual_mode = {
|
||||
caller = false :: false | () -> (),
|
||||
callbacks = {} :: { () -> () }
|
||||
|
|
@ -20,13 +46,14 @@ local manual_mode = {
|
|||
-- todo: rare case where mem address is reused by another function
|
||||
-- does this case handle itself?
|
||||
|
||||
-- registers a callback with the given lifetime using the given ref
|
||||
local function cleanup_ref(ref: string, lifetime: unknown, callback: () -> ())
|
||||
local id = ref_to_id[ref]
|
||||
|
||||
if id then
|
||||
if id then -- invoke previously registered callback then register new one
|
||||
cleanup_callbacks[id]()
|
||||
cleanup_lifetime[id] = lifetime -- rare case where ref is reused while lifetime is nil
|
||||
else
|
||||
else -- no previously registered callback, add and register new one
|
||||
id = #cleanup_callbacks + 1
|
||||
ref_to_id[ref] = id
|
||||
id_to_ref[id :: any] = ref -- todo
|
||||
|
|
@ -36,6 +63,7 @@ local function cleanup_ref(ref: string, lifetime: unknown, callback: () -> ())
|
|||
cleanup_callbacks[id] = callback
|
||||
end
|
||||
|
||||
-- registers a callback with its caller as the lifetime, and caller address as the ref
|
||||
local function cleanup(callback: () -> ())
|
||||
local lifetime = debug.info(2, "f") -- `caller of cleanup() is lifetime of cleanup`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
local Instance = Instance
|
||||
local typeof = typeof
|
||||
|
||||
if not game then
|
||||
script = require "test/relative-string"
|
||||
Instance = require("test/mock").Instance
|
||||
typeof = require("test/mock").typeof
|
||||
end
|
||||
if not game then script = require "test/relative-string" end
|
||||
local typeof = game and typeof or require "test/mock".typeof:: never
|
||||
local Instance = game and Instance or require "test/mock".Instance :: never
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local defaults = require(script.Parent.defaults)
|
||||
local apply = require(script.Parent.apply)
|
||||
local memoize = require(script.Parent.memoize)
|
||||
|
||||
local function create_instance(className: string)
|
||||
local success, instance: Instance = pcall(Instance.new, className :: any)
|
||||
if success == false then throw(`invalid class name, could not create instance of class { className }`) end
|
||||
local function create_instance(class_name: string)
|
||||
local ok, instance: Instance = pcall(Instance.new, class_name :: any)
|
||||
if not ok then throw(`invalid class name, could not create instance of class { class_name }`) end
|
||||
|
||||
local default: { [string]: unknown }? = defaults[className]
|
||||
local default: { [string]: unknown }? = defaults[class_name]
|
||||
if default then
|
||||
for i, v in next, default do
|
||||
(instance :: any)[i] = v
|
||||
|
|
@ -26,7 +21,7 @@ local function create_instance(className: string)
|
|||
return function(properties: { [any]: unknown }): Instance
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
end; create_instance = memoize(create_instance)
|
||||
end; create_instance = memoize(create_instance) -- always return same constructor for given class
|
||||
|
||||
local function clone_instance(instance: Instance)
|
||||
return function(properties: { [any]: unknown }): Instance
|
||||
|
|
@ -36,14 +31,15 @@ local function clone_instance(instance: Instance)
|
|||
end
|
||||
end
|
||||
|
||||
local function create(classNameOrInstance: string|Instance)
|
||||
if type(classNameOrInstance) == "string" then
|
||||
return create_instance(classNameOrInstance)
|
||||
elseif typeof(classNameOrInstance) == "Instance" then
|
||||
return clone_instance(classNameOrInstance)
|
||||
local function create(class_or_instance: string|Instance)
|
||||
if type(class_or_instance) == "string" then
|
||||
return create_instance(class_or_instance)
|
||||
elseif typeof(class_or_instance) == "Instance" then
|
||||
return clone_instance(class_or_instance)
|
||||
else
|
||||
error("Bad argument #1, expected string or instance, got "..typeof(classNameOrInstance), 2)
|
||||
throw("bad argument #1, expected string or instance, got "..typeof(class_or_instance))
|
||||
end
|
||||
return nil :: never
|
||||
end
|
||||
|
||||
type Props = { [any]: any }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
local Enum = Enum
|
||||
local Color3 = Color3
|
||||
|
||||
if not game then
|
||||
local mock = require "test/mock"
|
||||
Enum = mock.Enum
|
||||
Color3 = mock.Color3
|
||||
end
|
||||
local Enum = game and Enum or require "test/mock".Enum :: never
|
||||
local Color3 = game and Color3 or require "test/mock".Color3 :: never
|
||||
|
||||
return {
|
||||
Part = {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ local create = graph.create
|
|||
local capture_and_link = graph.capture_and_link
|
||||
|
||||
local function derive<T>(fn: () -> T): () -> T
|
||||
local node, node_get = create((nil :: any) :: T)
|
||||
local node, read_node_value = create((false :: any) :: T)
|
||||
|
||||
node.cache = capture_and_link(node, fn)
|
||||
|
||||
return node_get
|
||||
return read_node_value
|
||||
end
|
||||
|
||||
return derive
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ export type Node<T> = {
|
|||
children: { Node<T> } | false -- weak values
|
||||
}
|
||||
|
||||
-- flag used to detect when node reference capturing is active
|
||||
local reff = false
|
||||
-- array of all nodes referenced since above flag was set
|
||||
local refs = {} :: { Node<unknown> }
|
||||
|
||||
local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
|
||||
|
|
@ -18,6 +20,7 @@ local EVALUATION_ERR = "error while evaluating source:\n\n"
|
|||
|
||||
setmetatable(refs :: any, WEAK_VALUES_RESIZABLE)
|
||||
|
||||
-- runs a given callback in a context that Luau does not allow yielding in
|
||||
local check_for_yield: <T...>(fn: (T...) -> unknown, T...) -> () do
|
||||
local t = { __mode = "kv" }
|
||||
setmetatable(t, t)
|
||||
|
|
@ -43,12 +46,23 @@ local check_for_yield: <T...>(fn: (T...) -> unknown, T...) -> () do
|
|||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
|
||||
Each node side-effect is registered with a corresponding weak key.
|
||||
This makes the lifetime of the side-effect tied to the key's.
|
||||
The main usecase of this is to tie a side-effect to an instance, while allowing
|
||||
the instance to be garbage collected even when the node still exists.
|
||||
|
||||
The weak key is passed as an argument to its side-effect callback.
|
||||
|
||||
]]
|
||||
|
||||
local function set_effect<T>(node: Node<unknown>, fn: (T) -> (), key: T)
|
||||
node.effects[fn :: () -> ()] = key
|
||||
end
|
||||
|
||||
local function run_effects(node: Node<unknown>)
|
||||
if flags.strict then
|
||||
if flags.strict then -- run effects twice if strict
|
||||
for effect, key in next, node.effects do
|
||||
effect(key)
|
||||
effect(key)
|
||||
|
|
@ -61,12 +75,13 @@ local function run_effects(node: Node<unknown>)
|
|||
end
|
||||
|
||||
-- retrieves a node's cached value
|
||||
-- recalculates value if an ancestor was updated
|
||||
-- add self to refs if ref capture flag is enabled
|
||||
local function get<T>(node: Node<T>): T
|
||||
if reff then table.insert(refs, node) end
|
||||
return node.cache
|
||||
end
|
||||
|
||||
-- links two nodes as parent-child
|
||||
local function set_child(parent: Node<unknown>, child: Node<unknown>)
|
||||
if parent.children then
|
||||
table.insert(parent.children, child)
|
||||
|
|
@ -144,11 +159,11 @@ local function create<T>(value: T): (Node<T>, () -> T)
|
|||
children = false :: false
|
||||
}
|
||||
|
||||
local function get_value()
|
||||
local function read_node_value()
|
||||
return get(node)
|
||||
end
|
||||
|
||||
return node, get_value
|
||||
return node, read_node_value
|
||||
end
|
||||
|
||||
return table.freeze {
|
||||
|
|
|
|||
|
|
@ -52,17 +52,30 @@ local vide = {
|
|||
|
||||
-- runtime
|
||||
step = function(dt: number)
|
||||
-- debug.profilebegin("VIDE STEP")
|
||||
-- debug.profilebegin("VIDE SPRING")
|
||||
if game then
|
||||
debug.profilebegin("VIDE STEP")
|
||||
debug.profilebegin("VIDE SPRING")
|
||||
end
|
||||
|
||||
update_springs(dt)
|
||||
-- debug.profileend()
|
||||
-- debug.profilebegin("VIDE GARBAGE CLEANUP")
|
||||
|
||||
if game then
|
||||
debug.profileend()
|
||||
debug.profilebegin("VIDE GARBAGE CLEANUP")
|
||||
end
|
||||
|
||||
clean_garbage()
|
||||
-- debug.profileend()
|
||||
-- debug.profileend()
|
||||
|
||||
if game then
|
||||
debug.profileend()
|
||||
debug.profileend()
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
do
|
||||
local set = false
|
||||
|
||||
setmetatable(vide :: any, {
|
||||
__index = function(_, index: unknown): ()
|
||||
if index == "strict" then
|
||||
|
|
@ -74,13 +87,15 @@ setmetatable(vide :: any, {
|
|||
|
||||
__newindex = function(_, index: unknown, value: unknown)
|
||||
if index == "strict" then
|
||||
if value ~= true then throw "strict mode can only be set to true" end
|
||||
flags.strict = true
|
||||
if set then throw "strict mode has already been set" end
|
||||
set = true
|
||||
flags.strict = value :: boolean
|
||||
else
|
||||
throw(`{tostring(index)} is not a valid member of vide`)
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
if game then
|
||||
game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
return output_array
|
||||
end
|
||||
|
||||
local output, output_get = create(nil :: any)
|
||||
local output, read_output_value = create(nil :: any)
|
||||
|
||||
local function derive()
|
||||
return recompute(input())
|
||||
|
|
@ -108,7 +108,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
end
|
||||
end)
|
||||
|
||||
return output_get
|
||||
return read_output_value
|
||||
end
|
||||
|
||||
-- todo: optimize output array
|
||||
|
|
@ -182,7 +182,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
return output_array
|
||||
end
|
||||
|
||||
local output, output_get = create(nil :: any)
|
||||
local output, read_output_value = create(nil :: any)
|
||||
|
||||
local function derive()
|
||||
return recompute(input())
|
||||
|
|
@ -205,7 +205,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
end
|
||||
end)
|
||||
|
||||
return output_get
|
||||
return read_output_value
|
||||
end
|
||||
|
||||
return function() return indexes, values end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
local function memoize<X, Y>(f: (X) -> Y): ((X) -> Y, { [X]: Y })
|
||||
local cache: { [X]: Y } = {}
|
||||
local function memoize<X, Y>(f: (X) -> Y): (X) -> Y
|
||||
local cache: { [X]: Y? } = {}
|
||||
|
||||
return function(x: X): Y
|
||||
local y: Y? = cache[x]
|
||||
local y = cache[x]
|
||||
|
||||
if not y then
|
||||
y = f(x)
|
||||
|
|
@ -10,7 +10,7 @@ local function memoize<X, Y>(f: (X) -> Y): ((X) -> Y, { [X]: Y })
|
|||
end
|
||||
|
||||
return y :: Y
|
||||
end, cache
|
||||
end
|
||||
end
|
||||
|
||||
return memoize
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ type Node<T> = graph.Node<T>
|
|||
local create = graph.create
|
||||
local set = graph.set
|
||||
|
||||
|
||||
export type Source<T> = (() -> T) & ((T) -> T)
|
||||
|
||||
local function source<T>(value: T): Source<T>
|
||||
local node, get_value = create(value :: T)
|
||||
local node, read_node_value = create(value :: T)
|
||||
|
||||
return function(...): T
|
||||
if select("#", ...) == 0 then return get_value() end
|
||||
if select("#", ...) == 0 then return read_node_value() end -- check if any args were given
|
||||
|
||||
local v = ... :: T
|
||||
if node.cache == v and (type(v) ~= "table" or table.isfrozen(v)) then return v end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ Unsupported datatypes:
|
|||
]]
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create = graph.create
|
||||
|
|
@ -51,6 +50,7 @@ type SpringData<T> = {
|
|||
|
||||
type Lerp<T> = (initial: T, target: T, alpha: number) -> T
|
||||
|
||||
-- period, damping ratio, initial velocity, total time
|
||||
local function solve(T: number, z: number, u: number, t: number): number -- alpha
|
||||
local wn = 2*math.pi / T
|
||||
local wd = wn * math.sqrt(1 - z^2)
|
||||
|
|
@ -94,6 +94,8 @@ local lerpable: { [string]: Lerp<any> } = {
|
|||
end :: Lerp<Vector3>,
|
||||
}
|
||||
|
||||
-- maps spring data to its corresponding output node
|
||||
-- lifetime of spring data is tied to output node's
|
||||
local springs: { [SpringData<any>]: Node<any> } = {}
|
||||
setmetatable(springs, { __mode = "vs" })
|
||||
|
||||
|
|
@ -103,7 +105,6 @@ local function spring<T>(target: () -> T, period: number?, damping_ratio: number
|
|||
end
|
||||
|
||||
local inputs, initial_position = capture(target)
|
||||
|
||||
local output, output_get = create(initial_position)
|
||||
|
||||
local data: SpringData<T> = {
|
||||
|
|
@ -121,14 +122,16 @@ local function spring<T>(target: () -> T, period: number?, damping_ratio: number
|
|||
target = target
|
||||
}
|
||||
|
||||
local function input_changed(node)
|
||||
-- reschedule spring for simulation on input update
|
||||
local function input_updated(node)
|
||||
data.target_updated = true
|
||||
data.target_position = target()
|
||||
springs[data] = node
|
||||
end
|
||||
|
||||
-- register above function as side-effect for all inputs
|
||||
for _, input in next, inputs do
|
||||
set_effect(input, input_changed, output)
|
||||
set_effect(input, input_updated, output)
|
||||
end
|
||||
|
||||
springs[data] = output
|
||||
|
|
@ -136,6 +139,8 @@ local function spring<T>(target: () -> T, period: number?, damping_ratio: number
|
|||
return output_get
|
||||
end
|
||||
|
||||
-- `springs` is a hashmap, use array to queue indexes-to-remove to avoid
|
||||
-- iterator invalidation of `springs`
|
||||
local remove_queue = {}
|
||||
|
||||
local function update_springs(dt: number)
|
||||
|
|
@ -183,7 +188,7 @@ local function update_springs(dt: number)
|
|||
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, remove and set value to target
|
||||
-- close enough to target, unshedule spring and set value to target
|
||||
table.insert(remove_queue, data)
|
||||
set(output, target_position)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
-- returns path to file as an array with each directory
|
||||
-- accounts for Roblox and Luau contexts
|
||||
local function get_path(s)
|
||||
if string.sub(s, #s - 4, #s) == ".luau" then
|
||||
s = string.sub(s, 1, #s - 5)
|
||||
|
|
@ -6,11 +8,14 @@ local function get_path(s)
|
|||
return string.split(s, string.match(s, "%w+/") and "/" or ".")
|
||||
end
|
||||
|
||||
-- get directory of vide root
|
||||
local root do
|
||||
local path = get_path(debug.info(1, "s"))
|
||||
root = path[#path - 1]
|
||||
end
|
||||
|
||||
-- throws an error, ensuring stack trace begins at the first callsite outside
|
||||
-- of all vide library files
|
||||
local function throw(msg: string)
|
||||
local stack = 1
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ local refs = graph.refs
|
|||
|
||||
local function untrack<T>(source: () -> T): T
|
||||
local initial = #refs
|
||||
|
||||
local value = source()
|
||||
|
||||
-- remove any references made since `untrack()` was called
|
||||
for i = initial, #refs do
|
||||
refs[i] = nil
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@ local capture = graph.capture
|
|||
local function watch(effect: () -> ()): () -> ()
|
||||
local nodes = capture(effect :: () -> nil)
|
||||
|
||||
-- store aside captured nodes in new table
|
||||
nodes = table.clone(nodes)
|
||||
|
||||
-- register effect with permanent lifetime
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, effect, true)
|
||||
end
|
||||
|
||||
local function unwatch()
|
||||
-- unregister effect from all nodes
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, effect, nil)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue