Skip to main content

Selection operations

Most commands you'll write are some flavor of "do X to whatever's selected." A few patterns reduce the boilerplate.

The skeleton

--!strict
local Types = require(script.Types)

return {
Arguments = { ... },

Run = function(props: Types.Props, arguments: Types.ArgumentResult)
if #props.Selected == 0 then
return props:SendNotification("Select something first")
end

for _, inst in props.Selected do
-- ...do work on inst...
end
end,
}

Three things to internalize:

  • Always early-return on empty selection with a notification. Otherwise Run silently does nothing and the user has no idea why.
  • Iterate props.Selected, not Selection:Get(). The selection can change while Run is on the call stack. props.Selected is the snapshot from execute-time and stays stable.
  • Type-guard before assuming. props.Selected is {Instance}. Check with :IsA("BasePart") (or whatever) before touching type-specific properties.

Filtering selection by type

Most commands only care about a subset of what could be selected. Pull the filter out:

local function partsIn(list: {Instance}): {BasePart}
local out: {BasePart} = {}
for _, inst in list do
if inst:IsA("BasePart") then
table.insert(out, inst)
end
end
return out
end

return {
Arguments = { ... },

Run = function(props)
local parts = partsIn(props.Selected)
if #parts == 0 then
return props:SendNotification("Select at least one BasePart")
end

for _, part in parts do
-- ...
end
end,
}

Acting on descendants

If your command should "treat the selection as a tree," walk it with :GetDescendants():

Run = function(props, arguments)
for _, root in props.Selected do
for _, descendant in root:GetDescendants() do
if descendant:IsA("BasePart") then
descendant.Anchored = arguments.Anchor
end
end
end
end,

For huge selections this can be slow. Consider using a paragraph argument with a Lua expression the user can tune, or a boolean "Recursive" toggle.

Replacing the selection after the fact

A common pattern: a "duplicate" command should leave the new copies selected, not the originals. Update the selection at the end of Run:

Run = function(props)
local copies = {}
for _, source in props.Selected do
local copy = source:Clone()
copy.Parent = source.Parent
table.insert(copies, copy)
end

props:SetSelection(copies)
end,

Framing the camera

If the command operates somewhere far from the camera (e.g. you spawned new copies offset by 100 studs), focus on the result so the user can see it:

local copies = {}
-- ...spawn copies...

props:SetSelection(copies)
props:FocusView(copies, 4) -- second arg is extra padding in studs

A complete recipe, "scatter"

A command that scatters cloned copies of the selection randomly within a radius:

--!strict
local Types = require(script.Types)

return {
Arguments = {
Count = {
Type = "number",
Default = 10,
Min = 1,
Max = 200,
},
Radius = {
Type = "number",
Default = 20,
Min = 1,
Step = 0.5,
},
Anchor = {
Type = "switch",
Default = true,
Description = "Anchor each scattered copy",
},
},

Run = function(props: Types.Props, arguments: Types.ArgumentResult)
local source = props.FirstSelected
if not (source and source:IsA("BasePart")) then
return props:SendNotification("Select a single BasePart first")
end

local copies = table.create(arguments.Count)

for i = 1, arguments.Count do
local copy = source:Clone()

local theta = math.random() * math.pi * 2
local r = math.sqrt(math.random()) * arguments.Radius
copy.Position = source.Position
+ Vector3.new(math.cos(theta) * r, 0, math.sin(theta) * r)

copy.Anchored = arguments.Anchor
copy.Parent = source.Parent
copies[i] = copy
end

props:SetSelection(copies)
end,

OnExecuted = function(props, args, _result)
props:SendNotification(
string.format("Scattered %d copies", args.Count)
)
end,
}