mirror of
https://github.com/centau/vide.git
synced 2026-08-20 23:01:37 +00:00
Merge branch 'main' of https://github.com/centau/vide
This commit is contained in:
commit
63077355eb
66 changed files with 1741 additions and 1671 deletions
157
src/apply.luau
157
src/apply.luau
|
|
@ -1,24 +1,20 @@
|
|||
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 typeof = game and typeof or require "../test/mock".typeof :: never
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local _, is_action = require(script.Parent.action)()
|
||||
local graph = require(script.Parent.graph)
|
||||
local flags = require "./flags"
|
||||
local implicit_effect = require "./implicit_effect"
|
||||
local _, is_action = require "./action"()
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
|
||||
type Array<V> = { V }
|
||||
type ArrayOrV<V> = {ArrayOrV<V>} | V
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local free_caches: {
|
||||
type Cache = {
|
||||
-- event listeners to connect after properties are set
|
||||
events: Map<
|
||||
string, -- event name
|
||||
() -> () -- listener
|
||||
events: Array<
|
||||
| string -- 1. event name
|
||||
| () -> () -- 2. listener
|
||||
>,
|
||||
|
||||
-- actions to run after events are connected
|
||||
|
|
@ -33,18 +29,18 @@ local free_caches: {
|
|||
Map<string, true> -- set of property names
|
||||
>,
|
||||
|
||||
-- use stack instead of recursive function to process nesting layers one at time
|
||||
-- deeper-nested properties take precedence over shallower-nested ones
|
||||
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
||||
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||
nested_stack: { {} | number }
|
||||
}?
|
||||
}
|
||||
|
||||
local function borrow_caches(): typeof(assert(free_caches))
|
||||
if free_caches then
|
||||
local caches = free_caches :: typeof(assert(free_caches))
|
||||
free_caches = nil
|
||||
return caches
|
||||
local free_cache: Cache?
|
||||
|
||||
local function borrow_cache(): Cache
|
||||
if free_cache then
|
||||
local cache = free_cache
|
||||
free_cache = nil
|
||||
return cache
|
||||
else
|
||||
return {
|
||||
events = {},
|
||||
|
|
@ -59,36 +55,61 @@ local function borrow_caches(): typeof(assert(free_caches))
|
|||
end
|
||||
end
|
||||
|
||||
local function return_caches(caches: typeof(free_caches) )
|
||||
free_caches = caches
|
||||
local function return_cache(cache: Cache )
|
||||
free_cache = cache
|
||||
end
|
||||
|
||||
-- map of datatype names to class default constructor for aggregate init
|
||||
local aggregates = {}
|
||||
for name, class in {
|
||||
CFrame = CFrame,
|
||||
Color3 = Color3,
|
||||
UDim = UDim,
|
||||
UDim2 = UDim2,
|
||||
Vector2 = Vector2,
|
||||
Vector3 = Vector3,
|
||||
Rect = Rect
|
||||
} :: Map<string, { [string]: any }> do
|
||||
aggregates[name] = class.new
|
||||
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
||||
for property, value in properties do
|
||||
if property == "Parent" then continue end
|
||||
|
||||
if type(property) == "string" then
|
||||
if flags.strict then -- check for duplicate property assignment at nesting depth
|
||||
if cache.nested_debug[depth][property] then
|
||||
error(`duplicate property {property} at depth {depth}`, 0)
|
||||
end
|
||||
cache.nested_debug[depth][property] = true
|
||||
end
|
||||
|
||||
if type(value) == "function" then
|
||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||
table.insert(cache.events, property) -- add event name to buffer
|
||||
table.insert(cache.events, value :: () -> ()) -- add event listener to buffer
|
||||
else
|
||||
implicit_effect.property(instance, property, value :: () -> ()) -- create implicit effect for property
|
||||
end
|
||||
else
|
||||
(instance :: any)[property] = value -- set property
|
||||
end
|
||||
elseif type(property) == "number" then
|
||||
if type(value) == "function" then
|
||||
implicit_effect.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
|
||||
elseif type(value) == "table" then
|
||||
if is_action(value) then
|
||||
table.insert(cache.actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
elseif flags.defer_nested_properties then
|
||||
table.insert(cache.nested_stack, value :: {})
|
||||
table.insert(cache.nested_stack, depth + 1) -- push table to stack for later processing
|
||||
else
|
||||
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
|
||||
end
|
||||
else
|
||||
(value :: Instance).Parent = instance -- parent child
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- applies table of nested properties to an instance using full vide semantics
|
||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||
if not properties then
|
||||
throw("attempt to call a constructor returned by create() with no properties")
|
||||
error "attempt to call a constructor returned by create() with no properties"
|
||||
end
|
||||
|
||||
local strict = flags.strict
|
||||
|
||||
-- queue parent assignment if any for last
|
||||
local parent: unknown = properties.Parent
|
||||
|
||||
local caches = borrow_caches()
|
||||
local caches = borrow_cache()
|
||||
local events = caches.events
|
||||
local actions = caches.actions
|
||||
local nested_debug = caches.nested_debug
|
||||
|
|
@ -97,55 +118,15 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
|||
-- process all properties
|
||||
local depth = 1
|
||||
repeat
|
||||
for property, value in properties do
|
||||
if property == "Parent" then continue end
|
||||
|
||||
if type(property) == "string" then
|
||||
if strict then -- check for duplicate prop assignment at nesting depth
|
||||
if nested_debug[depth][property] then
|
||||
throw(`duplicate property {property} at depth {depth}`)
|
||||
end
|
||||
nested_debug[depth][property] = true
|
||||
end
|
||||
|
||||
if type(value) == "table" then -- attempt aggregate init
|
||||
local ctor = aggregates[typeof((instance :: any)[property])]
|
||||
if ctor == nil then
|
||||
throw(`cannot aggregate type {typeof(value)} for property {property}`)
|
||||
end
|
||||
(instance :: any)[property] = ctor(unpack(value :: {}))
|
||||
elseif type(value) == "function" then
|
||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||
events[property] = value :: () -> () -- add event to buffer
|
||||
else
|
||||
bind.property(instance, property, value :: () -> ()) -- bind property
|
||||
end
|
||||
else
|
||||
(instance :: any)[property] = value -- set property
|
||||
end
|
||||
elseif type(property) == "number" then
|
||||
if type(value) == "function" then
|
||||
bind.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
|
||||
elseif type(value) == "table" then
|
||||
if is_action(value) then
|
||||
table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
else
|
||||
table.insert(nested_stack, value :: {})
|
||||
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
|
||||
end
|
||||
else
|
||||
(value :: Instance).Parent = instance -- parent child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_properties(properties, instance, caches, depth)
|
||||
depth = table.remove(nested_stack) :: number
|
||||
properties = table.remove(nested_stack) :: {}
|
||||
|
||||
until not properties
|
||||
|
||||
for event, listener in next, events do
|
||||
(instance :: any)[event]:Connect(listener)
|
||||
for i = 1, #events, 2 do
|
||||
local event_name = events[i]
|
||||
local event_listener = events[i + 1]
|
||||
;(instance :: any)[event_name]:Connect(event_listener)
|
||||
end
|
||||
|
||||
for _, queued in next, actions do
|
||||
|
|
@ -154,22 +135,20 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
|||
end
|
||||
end
|
||||
|
||||
-- finally set parent if any
|
||||
if parent then
|
||||
if type(parent) == "function" then
|
||||
bind.parent(instance, parent :: () -> Instance)
|
||||
implicit_effect.parent(instance, parent :: () -> Instance)
|
||||
else
|
||||
instance.Parent = parent :: Instance
|
||||
end
|
||||
end
|
||||
|
||||
-- clear caches
|
||||
table.clear(events)
|
||||
for _, queued in next, actions do table.clear(queued) end
|
||||
if strict then table.clear(nested_debug) end
|
||||
if flags.strict then table.clear(nested_debug) end
|
||||
table.clear(nested_stack)
|
||||
|
||||
return_caches(caches)
|
||||
return_cache(caches)
|
||||
|
||||
return instance
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local flags = require "./flags"
|
||||
local graph = require "./graph"
|
||||
|
||||
local function batch(setter: () -> ())
|
||||
local already_batching = flags.batch
|
||||
|
|
@ -13,14 +10,14 @@ local function batch(setter: () -> ())
|
|||
from = graph.get_update_queue_length()
|
||||
end
|
||||
|
||||
local ok, err: string? = pcall(setter)
|
||||
local ok, err: string? = xpcall(setter, debug.traceback)
|
||||
|
||||
if not already_batching then
|
||||
flags.batch = false
|
||||
graph.flush_update_queue(from)
|
||||
end
|
||||
|
||||
if not ok then throw(`error occured while batching updates: {err}`) end
|
||||
if not ok then error(`error occured while batching updates: {err}`, 0) end
|
||||
end
|
||||
|
||||
return batch
|
||||
|
|
|
|||
105
src/bind.luau
105
src/bind.luau
|
|
@ -1,105 +0,0 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
||||
function create_implicit_effect<T>(updater: (T) -> T, binding: T)
|
||||
evaluate_node(create_node(assert_stable_scope(), updater, binding))
|
||||
end
|
||||
|
||||
type PropertyBinding = {
|
||||
instance: Instance,
|
||||
property: string,
|
||||
source: () -> unknown
|
||||
}
|
||||
|
||||
local function update_property_effect(p: PropertyBinding)
|
||||
(p.instance :: any)[p.property] = p.source()
|
||||
return p
|
||||
end
|
||||
|
||||
type ParentBinding = {
|
||||
instance: Instance,
|
||||
parent: () -> Instance
|
||||
}
|
||||
|
||||
local function update_parent_effect(p: ParentBinding)
|
||||
p.instance.Parent = p.parent()
|
||||
return p
|
||||
end
|
||||
|
||||
type ChildrenBinding = {
|
||||
instance: Instance,
|
||||
cur_children_set: { [Instance]: true },
|
||||
new_children_set: { [Instance]: true },
|
||||
children: () -> Instance | { Instance }
|
||||
}
|
||||
|
||||
type ArrayOrV<V> = V | { V }
|
||||
local function update_children_effect(p: ChildrenBinding)
|
||||
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
|
||||
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
|
||||
|
||||
local new_children = p.children() -- all (and only) children that should be parented after this update
|
||||
|
||||
if type(new_children) ~= "table" then
|
||||
new_children = { new_children }
|
||||
end
|
||||
|
||||
local function process_child(child: ArrayOrV<Instance>)
|
||||
if type(child) == "table" then
|
||||
for _, child in next, child do
|
||||
process_child(child)
|
||||
end
|
||||
else
|
||||
if new_child_set[child] then return end -- stops redundant reparenting
|
||||
|
||||
new_child_set[child] = true -- record child set from this update
|
||||
if not cur_children_set[child] then
|
||||
child.Parent = p.instance -- if child wasn't already parented then parent it
|
||||
else
|
||||
cur_children_set[child] = nil -- remove child from cache if it was already in cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_child(new_children)
|
||||
|
||||
for child in next, cur_children_set do
|
||||
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||
end
|
||||
|
||||
table.clear(cur_children_set) -- clear cache, preserve capacity
|
||||
p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
|
||||
|
||||
return p
|
||||
end
|
||||
|
||||
return {
|
||||
property = function(instance, property, source)
|
||||
return create_implicit_effect(update_property_effect, {
|
||||
instance = instance,
|
||||
property = property,
|
||||
source = source
|
||||
})
|
||||
end,
|
||||
|
||||
parent = function(instance, parent)
|
||||
return create_implicit_effect(update_parent_effect, {
|
||||
instance = instance,
|
||||
parent = parent
|
||||
})
|
||||
end,
|
||||
|
||||
children = function(instance, children)
|
||||
return create_implicit_effect(update_children_effect, {
|
||||
instance = instance,
|
||||
cur_children_set = {},
|
||||
new_children_set = {},
|
||||
children = children
|
||||
})
|
||||
end
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local action = require(script.Parent.action)()
|
||||
local cleanup = require(script.Parent.cleanup)
|
||||
local action = require "./action"()
|
||||
local cleanup = require "./cleanup"
|
||||
|
||||
local function changed<T>(property: string, callback: (T) -> ())
|
||||
return action(function(instance)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
local typeof = game and typeof or require "test/mock".typeof :: never
|
||||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
local get_scope = graph.get_scope
|
||||
local push_cleanup = graph.push_cleanup
|
||||
|
||||
local function helper(obj: any)
|
||||
return
|
||||
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
||||
elseif type(obj) == "thread" then function() task.cancel(obj) end
|
||||
elseif typeof(obj) == "Instance" then function() obj:Destroy() end
|
||||
elseif obj.destroy then function() obj:destroy() end
|
||||
elseif obj.disconnect then function() obj:disconnect() end
|
||||
elseif obj.Destroy then function() obj:Destroy() end
|
||||
elseif obj.Disconnect then function() obj:Disconnect() end
|
||||
else throw("cannot cleanup given object")
|
||||
else error "cannot cleanup given object"
|
||||
end
|
||||
|
||||
local function cleanup(value: unknown)
|
||||
local scope = get_scope()
|
||||
|
||||
if not scope then
|
||||
throw "cannot cleanup outside a stable or reactive scope"
|
||||
error "cannot cleanup outside a stable or reactive scope"
|
||||
end; assert(scope)
|
||||
|
||||
if type(value) == "function" then
|
||||
|
|
@ -36,6 +35,7 @@ type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
|||
|
||||
return cleanup ::
|
||||
( (callback: () -> ()) -> () ) &
|
||||
( (thread: thread) -> () ) &
|
||||
( (instance: Destroyable) -> () ) &
|
||||
( (connection: Disconnectable) -> () ) &
|
||||
( (instance: Instance) -> () ) &
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local get_scope = graph.get_scope
|
||||
|
|
@ -46,10 +43,10 @@ local function context<T>(...: T): Context<T>
|
|||
if has_default ~= nil then
|
||||
return default_value
|
||||
else
|
||||
throw("attempt to get context when no context is set and no default context is set")
|
||||
error("attempt to get context when no context is set and no default context is set", 0)
|
||||
end
|
||||
else -- set
|
||||
if not scope then return throw("attempt to set context outside of a vide scope") end
|
||||
if not scope then return error("attempt to set context outside of a vide scope", 0) end
|
||||
|
||||
local value, component = ...
|
||||
|
||||
|
|
@ -64,7 +61,7 @@ local function context<T>(...: T): Context<T>
|
|||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
throw(`error while running context:\n\n{result}`)
|
||||
error(`error while running context:\n\n{result}`, 0)
|
||||
end
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
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 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 r = require(script.Parent.roblox_types)
|
||||
local defaults = require "./defaults"
|
||||
local apply = require "./apply"
|
||||
|
||||
local ctor_cache = {} :: { [string]: () -> Instance }
|
||||
|
||||
setmetatable(ctor_cache :: any, {
|
||||
__index = function(self, class)
|
||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
||||
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
|
||||
if not ok then error(`invalid class name, could not create instance of class { class }`, 0) end
|
||||
|
||||
local default: { [string]: unknown }? = defaults[class]
|
||||
if default then
|
||||
|
|
@ -22,8 +19,8 @@ setmetatable(ctor_cache :: any, {
|
|||
end
|
||||
|
||||
local function ctor(properties: Props): Instance
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
|
||||
self[class] = ctor
|
||||
return ctor
|
||||
|
|
@ -37,20 +34,25 @@ end
|
|||
local function clone_instance(instance: Instance)
|
||||
return function(properties: Props): Instance
|
||||
local clone = instance:Clone()
|
||||
if not clone then throw "attempt to clone a non-archivable instance" end
|
||||
if not clone then error "attempt to clone a non-archivable instance" end
|
||||
return apply(clone, properties)
|
||||
end
|
||||
end
|
||||
|
||||
local function create(class_or_instance: string|Instance): (Props) -> 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
|
||||
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
||||
return nil :: never
|
||||
end
|
||||
local function create(class_or_instance: string | Instance, props: Props?): ((Props) -> Instance) | Instance
|
||||
local result: (Props) -> Instance
|
||||
if type(class_or_instance) == "string" then
|
||||
result = create_instance(class_or_instance)
|
||||
elseif typeof(class_or_instance) == "Instance" then
|
||||
result = clone_instance(class_or_instance)
|
||||
else
|
||||
error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
|
||||
return nil :: never
|
||||
end
|
||||
if props then
|
||||
return result(props)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
type Props = { [any]: any }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
local Enum = game and Enum or require "test/mock".Enum :: never
|
||||
local Color3 = game and Color3 or require "test/mock".Color3 :: never
|
||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||
local Enum = game and Enum or require "../test/mock".Enum :: never
|
||||
local Color3 = game and Color3 or require "../test/mock".Color3 :: never
|
||||
|
||||
return {
|
||||
Part = {
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
Size = Vector3.new(1, 1, 1),
|
||||
Size = vector.create(1, 1, 1),
|
||||
Anchored = true
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
local create_node = graph.create_node
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
local create_node = graph.create_node
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
|
|
|||
|
|
@ -4,4 +4,8 @@ end
|
|||
|
||||
local is_O2 = inline_test() ~= "inline_test"
|
||||
|
||||
return { strict = not is_O2, batch = false }
|
||||
return {
|
||||
strict = not is_O2,
|
||||
batch = false,
|
||||
defer_nested_properties = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
local flags = require "./flags"
|
||||
|
||||
export type SourceNode<T> = {
|
||||
cache: T,
|
||||
|
|
@ -24,9 +21,23 @@ export type Node<T> = {
|
|||
|
||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
||||
|
||||
local function efn(err: string)
|
||||
local trace = debug.traceback(err, 2)
|
||||
|
||||
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
||||
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
|
||||
return "\n "
|
||||
end)
|
||||
end
|
||||
|
||||
trace ..= "\nsource update stacktrace:"
|
||||
return trace
|
||||
end
|
||||
|
||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||
|
||||
local thread = coroutine.create(xpcall)
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
--local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||
|
||||
assert(resume_ok)
|
||||
|
|
@ -47,9 +58,9 @@ local function assert_stable_scope(): Node<unknown>
|
|||
|
||||
if not scope then
|
||||
local caller_name = debug.info(2, "n")
|
||||
return throw(`cannot use {caller_name}() outside a stable or reactive scope`)
|
||||
return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
|
||||
elseif scope.effect then
|
||||
throw("cannot create a new reactive scope inside another reactive scope")
|
||||
error("cannot create a new reactive scope inside another reactive scope", 0)
|
||||
end
|
||||
|
||||
return scope
|
||||
|
|
@ -83,8 +94,8 @@ end
|
|||
local function flush_cleanups<T>(node: Node<T>)
|
||||
if node.cleanups then
|
||||
for _, fn in next, node.cleanups do
|
||||
local ok, err: string? = pcall(fn)
|
||||
if not ok then throw(`cleanup error: {err}`) end
|
||||
local ok, err: string? = xpcall(fn, debug.traceback)
|
||||
if not ok then error(`cleanup error: {err}`, 0) end
|
||||
end
|
||||
|
||||
table.clear(node.cleanups)
|
||||
|
|
@ -108,6 +119,10 @@ local function unparent<T>(node: Node<T>)
|
|||
end
|
||||
|
||||
local function destroy<T>(node: Node<T>)
|
||||
if flags.strict and table.find(scopes, node) then
|
||||
error("attempt to destroy an active scope", 0)
|
||||
end
|
||||
|
||||
flush_cleanups(node)
|
||||
unparent(node)
|
||||
|
||||
|
|
@ -148,7 +163,7 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
throw(`effect stacktrace:\n{new_value :: string}`)
|
||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
||||
end
|
||||
|
||||
node.cache = new_value :: T
|
||||
|
|
@ -168,7 +183,7 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
throw(`effect stacktrace:\n{new_value}\n`)
|
||||
error(`effect error:\n{new_value}\n`, 0)
|
||||
end
|
||||
|
||||
node.cache = new_value
|
||||
|
|
@ -296,5 +311,7 @@ return table.freeze {
|
|||
flush_update_queue = flush_update_queue,
|
||||
get_update_queue_length = get_update_queue_length,
|
||||
set_context = set_context,
|
||||
scopes = scopes
|
||||
scopes = scopes,
|
||||
|
||||
q = update_queue
|
||||
}
|
||||
|
|
|
|||
119
src/implicit_effect.luau
Normal file
119
src/implicit_effect.luau
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local get_scope = graph.get_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local push_cleanup = graph.push_cleanup
|
||||
|
||||
local function update_property_effect(p: {
|
||||
instance: Instance,
|
||||
property: string,
|
||||
source: () -> unknown
|
||||
})
|
||||
(p.instance :: any)[p.property] = p.source()
|
||||
return p
|
||||
end
|
||||
|
||||
local function update_parent_effect(p: {
|
||||
instance: Instance,
|
||||
source: () -> Instance
|
||||
})
|
||||
p.instance.Parent = p.source()
|
||||
return p
|
||||
end
|
||||
|
||||
local function update_children_effect(p: {
|
||||
instance: Instance,
|
||||
cur_children_set: { [Instance]: true },
|
||||
new_children_set: { [Instance]: true },
|
||||
source: () -> Instance | { Instance }
|
||||
})
|
||||
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
|
||||
local new_children_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
|
||||
|
||||
local new_children = p.source() -- all (and only) children that should be parented after this update
|
||||
|
||||
local function process_child(child: Instance | { Instance })
|
||||
if type(child) == "userdata" then
|
||||
if new_children_set[child] then return end -- stops redundant reparenting
|
||||
|
||||
new_children_set[child] = true -- record child set from this update
|
||||
if not cur_children_set[child] then
|
||||
child.Parent = p.instance -- if child wasn't already parented then parent it
|
||||
else
|
||||
cur_children_set[child] = nil -- remove child from cache if it was already in cache
|
||||
end
|
||||
elseif type(child) == "table" then
|
||||
for _, child in next, child do
|
||||
process_child(child)
|
||||
end
|
||||
elseif type(child) == "function" then
|
||||
local node = create_node(assert(get_scope()), update_children_effect, {
|
||||
instance = p.instance,
|
||||
cur_children_set = {},
|
||||
new_children_set = {},
|
||||
source = child
|
||||
})
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
push_cleanup(assert(get_scope()), function()
|
||||
for child in node.cache.cur_children_set do
|
||||
child.Parent = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
process_child(new_children)
|
||||
|
||||
for child in next, cur_children_set do
|
||||
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||
end
|
||||
|
||||
table.clear(cur_children_set) -- clear cache, preserve capacity
|
||||
p.cur_children_set, p.new_children_set = new_children_set, cur_children_set
|
||||
|
||||
return p
|
||||
end
|
||||
|
||||
return {
|
||||
property = function(instance, property, source)
|
||||
local node = create_node(assert_stable_scope(), update_property_effect, {
|
||||
instance = instance,
|
||||
property = property,
|
||||
source = source
|
||||
})
|
||||
evaluate_node(node)
|
||||
return node
|
||||
end,
|
||||
|
||||
parent = function(instance, parent)
|
||||
local node = create_node(assert_stable_scope(), update_parent_effect, {
|
||||
instance = instance,
|
||||
source = parent
|
||||
})
|
||||
evaluate_node(node)
|
||||
return node
|
||||
end,
|
||||
|
||||
children = function(instance, children)
|
||||
local node = create_node(assert_stable_scope(), update_children_effect, {
|
||||
instance = instance,
|
||||
cur_children_set = {},
|
||||
new_children_set = {},
|
||||
source = children
|
||||
})
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
push_cleanup(assert_stable_scope(), function()
|
||||
for child in node.cache.cur_children_set do
|
||||
child.Parent = nil
|
||||
end
|
||||
end)
|
||||
|
||||
return node
|
||||
end
|
||||
}
|
||||
221
src/init.luau
221
src/init.luau
|
|
@ -1,219 +1,10 @@
|
|||
--------------------------------------------------------------------------------
|
||||
-- vide.luau
|
||||
--------------------------------------------------------------------------------
|
||||
assert(game, "when using vide outside of Roblox, require lib.luau instead")
|
||||
|
||||
local version = { major = 0, minor = 3, patch = 1 }
|
||||
local vide = require(script.lib)
|
||||
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local root = require(script.root)
|
||||
local mount = require(script.mount)
|
||||
local create = require(script.create)
|
||||
local apply = require(script.apply)
|
||||
local source = require(script.source)
|
||||
local effect = require(script.effect)
|
||||
local derive = require(script.derive)
|
||||
local cleanup = require(script.cleanup)
|
||||
local untrack = require(script.untrack)
|
||||
local read = require(script.read)
|
||||
local batch = require(script.batch)
|
||||
local context = require(script.context)
|
||||
local switch = require(script.switch)
|
||||
local show = require(script.show)
|
||||
local indexes, values = require(script.maps)()
|
||||
local spring, update_springs = require(script.spring)()
|
||||
local action = require(script.action)()
|
||||
local changed = require(script.changed)
|
||||
local throw = require(script.throw)
|
||||
local flags = require(script.flags)
|
||||
local roblox_types = require(script.roblox_types)
|
||||
|
||||
export type Source<T> = source.Source<T>
|
||||
export type source<T> = Source<T>
|
||||
export type Context<T> = context.Context<T>
|
||||
export type context<T> = Context<T>
|
||||
|
||||
local function step(dt: number)
|
||||
if game then
|
||||
debug.profilebegin("VIDE STEP")
|
||||
debug.profilebegin("VIDE SPRING")
|
||||
end
|
||||
|
||||
update_springs(dt)
|
||||
|
||||
if game then
|
||||
debug.profileend()
|
||||
debug.profileend()
|
||||
end
|
||||
end
|
||||
|
||||
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||
task.defer(step, dt)
|
||||
end)
|
||||
|
||||
local vide = {
|
||||
version = version,
|
||||
|
||||
-- core
|
||||
root = root,
|
||||
mount = mount,
|
||||
create = create,
|
||||
source = source,
|
||||
effect = effect,
|
||||
derive = derive,
|
||||
switch = switch,
|
||||
show = show,
|
||||
indexes = indexes,
|
||||
values = values,
|
||||
|
||||
-- util
|
||||
cleanup = cleanup,
|
||||
untrack = untrack,
|
||||
read = read,
|
||||
batch = batch,
|
||||
context = context,
|
||||
|
||||
-- animations
|
||||
spring = spring,
|
||||
|
||||
-- actions
|
||||
action = action,
|
||||
changed = changed,
|
||||
|
||||
-- flags
|
||||
strict = (nil :: any) :: boolean,
|
||||
|
||||
-- temporary
|
||||
apply = function(instance: Instance)
|
||||
return function(props: { [any]: any })
|
||||
apply(instance, props)
|
||||
return instance
|
||||
end
|
||||
end,
|
||||
|
||||
-- runtime
|
||||
step = function(dt: number)
|
||||
if stepped then
|
||||
stepped:Disconnect()
|
||||
stepped = nil
|
||||
end
|
||||
step(dt)
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable(vide :: any, {
|
||||
__index = function(_, index: unknown): ()
|
||||
if index == "strict" then
|
||||
return flags.strict
|
||||
else
|
||||
throw(`{tostring(index)} is not a valid member of vide`)
|
||||
end
|
||||
end,
|
||||
|
||||
__newindex = function(_, index: unknown, value: unknown)
|
||||
if index == "strict" then
|
||||
flags.strict = value :: boolean
|
||||
else
|
||||
throw(`{tostring(index)} is not a valid member of vide`)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
-- TYPES HERE
|
||||
export type vCanvasGroup = roblox_types.vCanvasGroup
|
||||
export type vFrame = roblox_types.vFrame
|
||||
export type vImageButton = roblox_types.vImageButton
|
||||
export type vTextButton = roblox_types.vTextButton
|
||||
export type vImageLabel = roblox_types.vImageLabel
|
||||
export type vTextLabel = roblox_types.vTextLabel
|
||||
export type vScrollingFrame = roblox_types.vScrollingFrame
|
||||
export type vTextBox = roblox_types.vTextBox
|
||||
export type vVideoFrame = roblox_types.vVideoFrame
|
||||
export type vViewportFrame = roblox_types.vViewportFrame
|
||||
export type vBillboardGui = roblox_types.vBillboardGui
|
||||
export type vScreenGui = roblox_types.vScreenGui
|
||||
export type vAdGui = roblox_types.vAdGui
|
||||
export type vSurfaceGui = roblox_types.vSurfaceGui
|
||||
export type vSelectionBox = roblox_types.vSelectionBox
|
||||
export type vBoxHandleAdornment = roblox_types.vBoxHandleAdornment
|
||||
export type vConeHandleAdornment = roblox_types.vConeHandleAdornment
|
||||
export type vCylinderHandleAdornment = roblox_types.vCylinderHandleAdornment
|
||||
export type vImageHandleAdornment = roblox_types.vImageHandleAdornment
|
||||
export type vLineHandleAdornment = roblox_types.vLineHandleAdornment
|
||||
export type vSphereHandleAdornment = roblox_types.vSphereHandleAdornment
|
||||
export type vWireframeHandleAdornment = roblox_types.vWireframeHandleAdornment
|
||||
export type vParabolaAdornment = roblox_types.vParabolaAdornment
|
||||
export type vSelectionSphere = roblox_types.vSelectionSphere
|
||||
export type vArcHandles = roblox_types.vArcHandles
|
||||
export type vHandles = roblox_types.vHandles
|
||||
export type vSurfaceSelection = roblox_types.vSurfaceSelection
|
||||
export type vPath2D = roblox_types.vPath2D
|
||||
export type vUIAspectRatioConstraint = roblox_types.vUIAspectRatioConstraint
|
||||
export type vUISizeConstraint = roblox_types.vUISizeConstraint
|
||||
export type vUITextSizeConstraint = roblox_types.vUITextSizeConstraint
|
||||
export type vUICorner = roblox_types.vUICorner
|
||||
export type vUIDragDetector = roblox_types.vUIDragDetector
|
||||
export type vUIFlexItem = roblox_types.vUIFlexItem
|
||||
export type vUIGradient = roblox_types.vUIGradient
|
||||
export type vUIListLayout = roblox_types.vUIListLayout
|
||||
export type vUIGridLayout = roblox_types.vUIGridLayout
|
||||
export type vUIPageLayout = roblox_types.vUIPageLayout
|
||||
export type vUITableLayout = roblox_types.vUITableLayout
|
||||
export type vUIPadding = roblox_types.vUIPadding
|
||||
export type vUIScale = roblox_types.vUIScale
|
||||
export type vUIStroke = roblox_types.vUIStroke
|
||||
export type vWorldModel = roblox_types.vWorldModel
|
||||
export type vCamera = roblox_types.vCamera
|
||||
export type vPart = roblox_types.vPart
|
||||
export type vModel = roblox_types.vModel
|
||||
export type vMeshPart = roblox_types.vMeshPart
|
||||
export type vHighlight = roblox_types.vHighlight
|
||||
export type vFrame = roblox_types.vFrame
|
||||
export type vImageButton = roblox_types.vImageButton
|
||||
export type vTextButton = roblox_types.vTextButton
|
||||
export type vImageLabel = roblox_types.vImageLabel
|
||||
export type vTextLabel = roblox_types.vTextLabel
|
||||
export type vScrollingFrame = roblox_types.vScrollingFrame
|
||||
export type vTextBox = roblox_types.vTextBox
|
||||
export type vVideoFrame = roblox_types.vVideoFrame
|
||||
export type vViewportFrame = roblox_types.vViewportFrame
|
||||
export type vBillboardGui = roblox_types.vBillboardGui
|
||||
export type vScreenGui = roblox_types.vScreenGui
|
||||
export type vAdGui = roblox_types.vAdGui
|
||||
export type vSurfaceGui = roblox_types.vSurfaceGui
|
||||
export type vSelectionBox = roblox_types.vSelectionBox
|
||||
export type vBoxHandleAdornment = roblox_types.vBoxHandleAdornment
|
||||
export type vConeHandleAdornment = roblox_types.vConeHandleAdornment
|
||||
export type vCylinderHandleAdornment = roblox_types.vCylinderHandleAdornment
|
||||
export type vImageHandleAdornment = roblox_types.vImageHandleAdornment
|
||||
export type vLineHandleAdornment = roblox_types.vLineHandleAdornment
|
||||
export type vSphereHandleAdornment = roblox_types.vSphereHandleAdornment
|
||||
export type vWireframeHandleAdornment = roblox_types.vWireframeHandleAdornment
|
||||
export type vParabolaAdornment = roblox_types.vParabolaAdornment
|
||||
export type vSelectionSphere = roblox_types.vSelectionSphere
|
||||
export type vArcHandles = roblox_types.vArcHandles
|
||||
export type vHandles = roblox_types.vHandles
|
||||
export type vSurfaceSelection = roblox_types.vSurfaceSelection
|
||||
export type vPath2D = roblox_types.vPath2D
|
||||
export type vUIAspectRatioConstraint = roblox_types.vUIAspectRatioConstraint
|
||||
export type vUISizeConstraint = roblox_types.vUISizeConstraint
|
||||
export type vUITextSizeConstraint = roblox_types.vUITextSizeConstraint
|
||||
export type vUICorner = roblox_types.vUICorner
|
||||
export type vUIDragDetector = roblox_types.vUIDragDetector
|
||||
export type vUIFlexItem = roblox_types.vUIFlexItem
|
||||
export type vUIGradient = roblox_types.vUIGradient
|
||||
export type vUIListLayout = roblox_types.vUIListLayout
|
||||
export type vUIGridLayout = roblox_types.vUIGridLayout
|
||||
export type vUIPageLayout = roblox_types.vUIPageLayout
|
||||
export type vUITableLayout = roblox_types.vUITableLayout
|
||||
export type vUIPadding = roblox_types.vUIPadding
|
||||
export type vUIScale = roblox_types.vUIScale
|
||||
export type vUIStroke = roblox_types.vUIStroke
|
||||
export type vWorldModel = roblox_types.vWorldModel
|
||||
export type vCamera = roblox_types.vCamera
|
||||
export type vPart = roblox_types.vPart
|
||||
export type vModel = roblox_types.vModel
|
||||
export type vMeshPart = roblox_types.vMeshPart
|
||||
export type vHighlight = roblox_types.vHighlight
|
||||
export type source<T> = vide.source<T>
|
||||
export type Source<T> = vide.Source<T>
|
||||
export type context<T> = vide.context<T>
|
||||
export type Context<T> = vide.Context<T>
|
||||
|
||||
return vide
|
||||
115
src/lib.luau
Normal file
115
src/lib.luau
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
local version = { major = 0, minor = 3, patch = 1 }
|
||||
|
||||
local root = require "./root"
|
||||
local mount = require "./mount"
|
||||
local create = require "./create"
|
||||
local apply = require "./apply"
|
||||
local source = require "./source"
|
||||
local effect = require "./effect"
|
||||
local derive = require "./derive"
|
||||
local cleanup = require "./cleanup"
|
||||
local untrack = require "./untrack"
|
||||
local read = require "./read"
|
||||
local batch = require "./batch"
|
||||
local context = require "./context"
|
||||
local switch = require "./switch"
|
||||
local show = require "./show"
|
||||
local indexes, values = require "./maps"()
|
||||
local spring, update_springs = require "./spring"()
|
||||
local action = require "./action"()
|
||||
local changed = require "./changed"
|
||||
local flags = require "./flags"
|
||||
|
||||
export type Source<T> = source.Source<T>
|
||||
export type source<T> = Source<T>
|
||||
export type Context<T> = context.Context<T>
|
||||
export type context<T> = Context<T>
|
||||
|
||||
local function step(dt: number)
|
||||
if game then
|
||||
debug.profilebegin("VIDE STEP")
|
||||
debug.profilebegin("VIDE SPRING")
|
||||
end
|
||||
|
||||
update_springs(dt)
|
||||
|
||||
if game then
|
||||
debug.profileend()
|
||||
debug.profileend()
|
||||
end
|
||||
end
|
||||
|
||||
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||
task.defer(step, dt)
|
||||
end)
|
||||
|
||||
local vide = {
|
||||
version = version,
|
||||
|
||||
-- core
|
||||
root = root,
|
||||
mount = mount,
|
||||
create = create,
|
||||
source = source,
|
||||
effect = effect,
|
||||
derive = derive,
|
||||
switch = switch,
|
||||
show = show,
|
||||
indexes = indexes,
|
||||
values = values,
|
||||
|
||||
-- util
|
||||
cleanup = cleanup,
|
||||
untrack = untrack,
|
||||
read = read,
|
||||
batch = batch,
|
||||
context = context,
|
||||
|
||||
-- animations
|
||||
spring = spring,
|
||||
|
||||
-- actions
|
||||
action = action,
|
||||
changed = changed,
|
||||
|
||||
-- flags
|
||||
strict = (nil :: any) :: boolean,
|
||||
defer_nested_properties = (nil :: any) :: boolean,
|
||||
|
||||
-- temporary
|
||||
apply = function(instance: Instance)
|
||||
return function(props: { [any]: any })
|
||||
apply(instance, props)
|
||||
return instance
|
||||
end
|
||||
end,
|
||||
|
||||
-- runtime
|
||||
step = function(dt: number)
|
||||
if stepped then
|
||||
stepped:Disconnect()
|
||||
stepped = nil
|
||||
end
|
||||
step(dt)
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable(vide :: any, {
|
||||
__index = function(_, index: unknown): ()
|
||||
if flags[index] == nil then
|
||||
error(`{tostring(index)} is not a valid member of vide`, 0)
|
||||
else
|
||||
return flags[index]
|
||||
end
|
||||
end,
|
||||
|
||||
__newindex = function(_, index: unknown, value: unknown)
|
||||
if flags[index] == nil then
|
||||
error(`{tostring(index)} is not a valid member of vide, 0`)
|
||||
else
|
||||
flags[index] = value
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
return vide
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
local graph = require(script.Parent.graph)
|
||||
local flags = require "./flags"
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
|
|
@ -22,7 +19,7 @@ local function check_primitives(t: {})
|
|||
|
||||
for _, v in next, t do
|
||||
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
|
||||
throw("table source map cannot return primitives")
|
||||
error("table source map cannot return primitives", 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -63,6 +60,8 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
local cv = input_cache[i]
|
||||
|
||||
if cv ~= v then
|
||||
input_cache[i] = v
|
||||
|
||||
if cv == nil then -- create new scope and run transform
|
||||
local scope = create_node(subowner, false, false)
|
||||
scopes[i] = scope :: Node<any>
|
||||
|
|
@ -71,7 +70,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, function()
|
||||
local ok, result = xpcall(transform, debug.traceback, function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end, i)
|
||||
|
|
@ -89,8 +88,6 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
input_nodes[i].cache = v
|
||||
update_descendants(input_nodes[i])
|
||||
end
|
||||
|
||||
input_cache[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -134,7 +131,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
local cache = {}
|
||||
for _, v in next, data do
|
||||
if cache[v] ~= nil then
|
||||
throw "duplicate table value detected"
|
||||
error "duplicate table value detected"
|
||||
end
|
||||
cache[v] = true
|
||||
end
|
||||
|
|
@ -156,7 +153,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, v, function()
|
||||
local ok, result = xpcall(transform, debug.traceback, v, function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local root = require(script.Parent.root)
|
||||
local apply = require(script.Parent.apply)
|
||||
local root = require "./root"
|
||||
local apply = require "./apply"
|
||||
|
||||
local function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||
return root(function()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local function read<T>(value: T | () -> T): T
|
||||
return if type(value) == "function" then value() else value
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local push_scope = graph.push_scope
|
||||
|
|
@ -16,21 +13,20 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
|
|||
refs[node] = true -- prevent gc of root node
|
||||
|
||||
local destroy = function()
|
||||
if not refs[node] then throw "root already destroyed" end
|
||||
if not refs[node] then error "root already destroyed" end
|
||||
refs[node] = nil
|
||||
destroy(node)
|
||||
end
|
||||
|
||||
push_scope(node)
|
||||
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local result = { xpcall(fn, efn, destroy) }
|
||||
local result = { xpcall(fn, debug.traceback, destroy) }
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not result[1] then
|
||||
destroy()
|
||||
throw(`error while running root():\n\n{result[2]}`)
|
||||
error(`error while running root():\n\n{result[2]}`, 0)
|
||||
end
|
||||
|
||||
return destroy, unpack(result :: any, 2)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local switch = require(script.Parent.switch)
|
||||
local switch = require "./switch"
|
||||
|
||||
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
||||
local function truthy()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_source_node = graph.create_source_node
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
|
|
@ -11,7 +9,7 @@ export type Source<T> = (() -> T) & ((value: T) -> T)
|
|||
local function source<T>(initial_value: T): Source<T>
|
||||
local node = create_source_node(initial_value)
|
||||
|
||||
return function(...): T
|
||||
local function update_source(...): T
|
||||
if select("#", ...) == 0 then -- no args were given
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
|
|
@ -26,6 +24,8 @@ local function source<T>(initial_value: T): Source<T>
|
|||
update_descendants(node)
|
||||
return v
|
||||
end
|
||||
|
||||
return update_source
|
||||
end
|
||||
|
||||
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
||||
|
|
|
|||
166
src/spring.luau
166
src/spring.luau
|
|
@ -1,28 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||
|
||||
--[[
|
||||
|
||||
Supported datatypes:
|
||||
- number
|
||||
- CFrame
|
||||
- Color3
|
||||
- UDim
|
||||
- UDim2
|
||||
- Vector2
|
||||
- Vector3
|
||||
- Rect
|
||||
|
||||
Unsupported datatypes:
|
||||
- bool
|
||||
- Vector2int16
|
||||
- Vector3int16
|
||||
- EnumItem
|
||||
|
||||
]]
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
|
|
@ -33,70 +9,71 @@ local update_descendants = graph.update_descendants
|
|||
local push_child_to_scope = graph.push_child_to_scope
|
||||
|
||||
local UPDATE_RATE = 120
|
||||
local TOLERANCE = 0.0001
|
||||
|
||||
type Vec3 = Vector3
|
||||
|
||||
local function Vec3(x: number?, y: number?, z: number?)
|
||||
return Vector3.new(x, y, z)
|
||||
end
|
||||
|
||||
local ZERO = Vec3(0, 0, 0)
|
||||
local TOLERANCE = 0.001
|
||||
local TOLERANCE_VECTOR = vector.create(TOLERANCE, TOLERANCE, TOLERANCE)
|
||||
|
||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||
|
||||
type SpringData<T> = {
|
||||
--[[
|
||||
Unsupported datatypes:
|
||||
- bool
|
||||
- Vector2int16
|
||||
- Vector3int16
|
||||
- EnumItem
|
||||
]]
|
||||
|
||||
type SpringState<T> = {
|
||||
k: number, -- spring constant
|
||||
c: number, -- damping coeff
|
||||
|
||||
-- dimensions 1-3
|
||||
x0_123: Vec3,
|
||||
x1_123: Vec3,
|
||||
v_123: Vec3,
|
||||
|
||||
-- dimensions 4-6
|
||||
x0_456: Vec3,
|
||||
x1_456: Vec3,
|
||||
v_456: Vec3,
|
||||
x0_123: vector, x0_456: vector, -- current position
|
||||
x1_123: vector, x1_456: vector, -- target position
|
||||
v_123: vector, v_456: vector, -- current velocity
|
||||
|
||||
source_value: T -- current value of spring input source
|
||||
}
|
||||
|
||||
type TypeToVec6<T> = (T) -> (Vec3, Vec3)
|
||||
type Vec6ToType<T> = (Vec3, Vec3) -> T
|
||||
type SpringSettings<T> = ({
|
||||
position: T?,
|
||||
velocity: T?,
|
||||
impulse: T?
|
||||
}) -> ()
|
||||
|
||||
type TypeToVec6<T> = (T) -> (vector, vector)
|
||||
type Vec6ToType<T> = (vector, vector) -> T
|
||||
|
||||
local type_to_vec6 = {
|
||||
number = function(v)
|
||||
return Vec3(v, 0, 0), ZERO
|
||||
return vector.create(v, 0, 0), vector.zero
|
||||
end :: TypeToVec6<number>,
|
||||
|
||||
CFrame = function(v)
|
||||
return v.Position, Vec3(v:ToEulerAnglesXYZ())
|
||||
return v.Position, vector.create(v:ToEulerAnglesXYZ())
|
||||
end :: TypeToVec6<CFrame>,
|
||||
|
||||
Color3 = function(v)
|
||||
-- todo: hsv, oklab?
|
||||
return Vec3(v.R, v.G, v.B), ZERO
|
||||
return vector.create(v.R, v.G, v.B), vector.zero
|
||||
end :: TypeToVec6<Color3>,
|
||||
|
||||
UDim = function(v)
|
||||
return Vec3(v.Scale, v.Offset, 0), ZERO
|
||||
return vector.create(v.Scale, v.Offset, 0), vector.zero
|
||||
end :: TypeToVec6<UDim>,
|
||||
|
||||
UDim2 = function(v)
|
||||
return Vec3(v.X.Scale, v.X.Offset, v.Y.Scale), Vec3(v.Y.Offset, 0, 0)
|
||||
return vector.create(v.X.Scale, v.X.Offset, v.Y.Scale), vector.create(v.Y.Offset, 0, 0)
|
||||
end :: TypeToVec6<UDim2>,
|
||||
|
||||
Vector2 = function(v)
|
||||
return Vec3(v.X, v.Y, 0), ZERO
|
||||
return vector.create(v.X, v.Y, 0), vector.zero
|
||||
end :: TypeToVec6<Vector2>,
|
||||
|
||||
Vector3 = function(v)
|
||||
return v, ZERO
|
||||
return v, vector.zero
|
||||
end :: TypeToVec6<Vector3>,
|
||||
|
||||
Rect = function(v)
|
||||
return Vec3(v.Min.X, v.Min.Y, v.Max.X), Vec3(v.Max.Y, 0, 0)
|
||||
return vector.create(v.Min.X, v.Min.Y, v.Max.X), vector.create(v.Max.Y, 0, 0)
|
||||
end :: TypeToVec6<Rect>
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +113,7 @@ local vec6_to_type = {
|
|||
|
||||
local invalid_type = {
|
||||
__index = function(_, t: string)
|
||||
throw(`cannot spring type {t}`)
|
||||
error(`cannot spring type {t}`, 0)
|
||||
end
|
||||
}
|
||||
|
||||
|
|
@ -145,10 +122,10 @@ setmetatable(vec6_to_type, invalid_type)
|
|||
|
||||
-- maps spring data to its corresponding output node
|
||||
-- lifetime of spring data is tied to output node
|
||||
local springs: { [SpringData<any>]: SourceNode<any> } = {}
|
||||
setmetatable(springs, { __mode = "v" })
|
||||
local springs: { [SpringState<unknown>]: SourceNode<unknown> } = {}
|
||||
setmetatable(springs :: any, { __mode = "v" })
|
||||
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): (() -> T, SpringSettings<T>)
|
||||
local owner = assert_stable_scope()
|
||||
|
||||
-- https://en.wikipedia.org/wiki/Damping
|
||||
|
|
@ -163,20 +140,20 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
-- todo: is there a solution other than reducing step size?
|
||||
-- todo: this does not catch all solver exploding cases
|
||||
if c > UPDATE_RATE*2 then -- solver will explode if this is true
|
||||
throw("spring damping too high, consider reducing damping or increasing period")
|
||||
error("spring damping too high, consider reducing damping or increasing period", 0)
|
||||
end
|
||||
|
||||
local data: SpringData<T> = {
|
||||
local data: SpringState<T> = {
|
||||
k = k,
|
||||
c = c,
|
||||
|
||||
x0_123 = ZERO,
|
||||
x1_123 = ZERO,
|
||||
v_123 = ZERO,
|
||||
x0_123 = vector.zero,
|
||||
x1_123 = vector.zero,
|
||||
v_123 = vector.zero,
|
||||
|
||||
x0_456 = ZERO,
|
||||
x1_456 = ZERO,
|
||||
v_456 = ZERO,
|
||||
x0_456 = vector.zero,
|
||||
x1_456 = vector.zero,
|
||||
v_456 = vector.zero,
|
||||
|
||||
source_value = false :: any,
|
||||
}
|
||||
|
|
@ -187,7 +164,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
local value = source()
|
||||
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
|
||||
data.source_value = value
|
||||
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
|
||||
springs[data] = output
|
||||
return value
|
||||
end
|
||||
|
||||
|
|
@ -201,6 +178,28 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
-- set output to goal
|
||||
output.cache = data.source_value
|
||||
|
||||
local setter = function(p)
|
||||
local x = p.position
|
||||
local v = p.velocity
|
||||
local dv = p.impulse
|
||||
|
||||
if x then
|
||||
data.x0_123, data.x0_456 = type_to_vec6[typeof(x)](x)
|
||||
end
|
||||
|
||||
if v then
|
||||
data.v_123, data.v_456 = type_to_vec6[typeof(v)](v)
|
||||
end
|
||||
|
||||
if dv then
|
||||
local dv_123, dv_456 = type_to_vec6[typeof(dv)](dv)
|
||||
data.v_123 += dv_123
|
||||
data.v_456 += dv_456
|
||||
end
|
||||
|
||||
springs[data] = output
|
||||
end :: SpringSettings<T>
|
||||
|
||||
return function(...)
|
||||
if select("#", ...) == 0 then -- no args were given
|
||||
push_child_to_scope(output)
|
||||
|
|
@ -212,8 +211,8 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
data.x0_123, data.x0_456 = type_to_vec6[typeof(v)](v)
|
||||
|
||||
-- reset velocity
|
||||
data.v_123 = ZERO
|
||||
data.v_456 = ZERO
|
||||
data.v_123 = vector.zero
|
||||
data.v_456 = vector.zero
|
||||
|
||||
-- schedule spring
|
||||
springs[data] = output
|
||||
|
|
@ -222,7 +221,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
output.cache = v
|
||||
|
||||
return v
|
||||
end
|
||||
end, setter
|
||||
end
|
||||
|
||||
local function step_springs(dt: number)
|
||||
|
|
@ -263,23 +262,24 @@ local function step_springs(dt: number)
|
|||
end
|
||||
end
|
||||
|
||||
local remove_queue = {}
|
||||
|
||||
local function update_spring_sources()
|
||||
for data, output in next, springs do
|
||||
for data, output in springs do
|
||||
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
|
||||
|
||||
local dx_123, dx_456 =
|
||||
x0_123 - x1_123,
|
||||
x0_456 - x1_456
|
||||
|
||||
-- todo: can this false positive?
|
||||
if (v_123 + v_456 + dx_123 + dx_456).Magnitude < TOLERANCE then
|
||||
local max_difference = vector.max(
|
||||
vector.abs(x0_123 - x1_123 :: any),
|
||||
vector.abs(x0_456 - x1_456 :: any),
|
||||
vector.abs(v_123 :: any),
|
||||
vector.abs(v_456 :: any),
|
||||
TOLERANCE_VECTOR
|
||||
)
|
||||
|
||||
if max_difference == TOLERANCE_VECTOR then
|
||||
-- close enough to target, unshedule spring and set value to target
|
||||
table.insert(remove_queue, data)
|
||||
springs[data] = nil
|
||||
output.cache = data.source_value
|
||||
else
|
||||
output.cache = vec6_to_type[typeof(data.source_value)](x0_123, x0_456)
|
||||
|
|
@ -287,12 +287,6 @@ local function update_spring_sources()
|
|||
|
||||
update_descendants(output)
|
||||
end
|
||||
|
||||
for _, data in next, remove_queue do
|
||||
springs[data] = nil
|
||||
end
|
||||
|
||||
table.clear(remove_queue)
|
||||
end
|
||||
|
||||
return function()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
|
|
@ -34,7 +31,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
|||
if component == nil then return nil end
|
||||
|
||||
if type(component) ~= "function" then
|
||||
throw "map must map a value to a function"
|
||||
error "map must map a value to a function"
|
||||
end
|
||||
|
||||
local new_scope = create_node(owner, false, false)
|
||||
|
|
@ -42,7 +39,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
|||
|
||||
push_scope(new_scope)
|
||||
|
||||
local ok, result = pcall(component)
|
||||
local ok, result = xpcall(component, debug.traceback)
|
||||
|
||||
pop_scope()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local function VIDE_ASSERT(msg): any
|
||||
error(msg, 0)
|
||||
end
|
||||
|
||||
return VIDE_ASSERT
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local get_scope = graph.get_scope
|
||||
|
||||
|
|
@ -12,13 +10,13 @@ local function untrack<T>(source: () -> T): T
|
|||
local effect = scope.effect
|
||||
scope.effect = false
|
||||
|
||||
local ok, result = pcall(source)
|
||||
local ok, result = xpcall(source, debug.traceback)
|
||||
|
||||
scope.effect = effect :: () -> ()
|
||||
|
||||
if not ok then error(result, 0) end
|
||||
|
||||
return result
|
||||
return result :: T
|
||||
else
|
||||
return source()
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue