#Classes

A custom class is a name you can put in ClassName that Vision resolves to a real instance plus a tree of its own.

Vision.class("Chip", {
    Base = "Frame",

    Properties = { Image = "", Text = "" },

    Construct = function(Made)
        return {
            { ClassName = "ImageLabel", Name = "Icon", Image = Made.Image },
            { ClassName = "TextLabel", Name = "Label", Text = Made.Text },
        }
    end,

    Changed = {
        Text = function(self, _, Value)
            self:FindFirstChild("Label").Text = Value
        end,
    },
})
Scope:Capture({
    ClassName = "Chip",
    Size = UDim2.fromOffset(200, 32),

    Image = "rbxassetid://100",
    Text = "hello",
})

Every creation checks the registry first. A ClassName that is registered builds through the class; anything else goes straight to Instance.new as before, so nothing changes for declarations that do not use one.

#The definition

Vision.class(Name: string, Definition) -> string
field what it is
Base required, the real ClassName the class is built on
Construct required, a function taking the resolved properties and returning a declaration
Properties a map of custom property names to default values
Changed a map of property names to callbacks, run when a bound value moves

Construct may return one declaration, or a table whose array part is a list of them. Both of these are fine:

Construct = function(Made)
    return { ClassName = "UICorner", CornerRadius = UDim.new(0, 4) }
end
Construct = function(Made)
    return {
        { ClassName = "UIListLayout" },
        { ClassName = "TextLabel", Text = Made.Text },
    }
end

What it returns becomes children of the instance, built before any children you declared yourself.

#Custom properties

A key that matches one of the class's Properties is not written to the instance. It is collected, defaulted, and handed to Construct as Made.

{
    ClassName = "Chip",

    Size = UDim2.fromOffset(200, 32),   -- a real Frame property, assigned
    Text = "hello",                     -- a custom property, given to Construct
}

They are not properties and they are not attributes. Nothing about the class is written onto the instance, so Body.Text and Body:GetAttribute("Text") are both nil. The class's own tree is where the value ended up.

Every property needs a default. Leave one out of a declaration and the default is what Construct sees.

#Changing one afterwards

Bind it to a value with fromEvent. The class's Changed callback for that property runs on every write, with the usual (instance, vision, value):

Scope:Capture({
    ClassName = "Frame",

    event("Word", "one", function() end),

    { ClassName = "Chip", Text = fromEvent("Word") },
})

Interface.Word("two") runs Chip's Changed.Text against that node. Any number of nodes can follow the same value, and one write moves all of them.

The constructor sees the value too. Construct is handed the value's current setting rather than the property default, so the first build is already correct rather than being corrected a moment later.

Binding a property the class does not watch is an error, because nothing would happen:

class "Chip" cannot follow a value on "TextSize", it does not watch that property

#Rules

#Adding one to your own game

Register before you capture anything that uses it. Anywhere that runs once is fine - the top of a module, or a small file that only registers:

local Vision = require(ReplicatedStorage.Packages.Vision)

Vision.class("Chip", { ... })

return true
require(script.Parent.Classes.Chip)

local Card = Scope:Capture({ ClassName = "Chip", Text = "hello" })

The registry is global to the Vision module, so a class registered on the client is not registered on the server. Register on both if both build it.

#Adding one to Vision itself

The same call, from inside the library. Put the class in its own module under src/shared/Vision/, require it from init.luau so registration happens when Vision is first required, and add it to the module list in scripts/build.luau's harness so it ships.

Vision does not ship any classes of its own. Every class is one you or your game registered, which keeps the library's surface to the primitives and leaves naming to you.