This commit is contained in:
Alice 2024-12-29 21:38:00 +01:00
commit 63077355eb
66 changed files with 1741 additions and 1671 deletions

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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) -> () ) &

View file

@ -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

View file

@ -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 }

View file

@ -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
},

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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
View 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
}

View file

@ -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
View 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

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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>)

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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