first commit

This commit is contained in:
2025-09-14 13:34:16 +02:00
commit 1003e84648
44 changed files with 11479 additions and 0 deletions

27
.eslintrc Normal file
View File

@@ -0,0 +1,27 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"jsx": true,
"useJSXTextNode": true,
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
},
"ignorePatterns": [
"/out"
],
"plugins": [
"@typescript-eslint",
"roblox-ts",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:roblox-ts/recommended-legacy",
"plugin:prettier/recommended"
],
"rules": {
"prettier/prettier": "warn"
}
}

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/node_modules
/out

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"printWidth": 120,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": true
}

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"roblox-ts.vscode-roblox-ts",
"dbaeumer.vscode-eslint"
]
}

15
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"files.eol": "\n",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.formatOnSave": true
},
"eslint.run": "onType",
"eslint.format.enable": true,
"eslint.useFlatConfig": false
}

19
default.project.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "roblox-ts-plugin",
"globIgnorePaths": [
"**/package.json",
"**/tsconfig.json"
],
"tree": {
"$path": "out",
"include": {
"$path": "include",
"node_modules": {
"$className": "Folder",
"@rbxts": {
"$path": "node_modules/@rbxts"
}
}
}
}
}

2068
include/Promise.lua Normal file

File diff suppressed because it is too large Load Diff

260
include/RuntimeLib.lua Normal file
View File

@@ -0,0 +1,260 @@
local Promise = require(script.Parent.Promise)
local RunService = game:GetService("RunService")
local OUTPUT_PREFIX = "roblox-ts: "
local NODE_MODULES = "node_modules"
local DEFAULT_SCOPE = "@rbxts"
local TS = {}
TS.Promise = Promise
local function isPlugin(context)
return RunService:IsStudio() and context:FindFirstAncestorWhichIsA("Plugin") ~= nil
end
function TS.getModule(context, scope, moduleName)
-- legacy call signature
if moduleName == nil then
moduleName = scope
scope = DEFAULT_SCOPE
end
-- ensure modules have fully replicated
if RunService:IsRunning() and RunService:IsClient() and not isPlugin(context) and not game:IsLoaded() then
game.Loaded:Wait()
end
local object = context
repeat
local nodeModulesFolder = object:FindFirstChild(NODE_MODULES)
if nodeModulesFolder then
local scopeFolder = nodeModulesFolder:FindFirstChild(scope)
if scopeFolder then
local module = scopeFolder:FindFirstChild(moduleName)
if module then
return module
end
end
end
object = object.Parent
until object == nil
error(OUTPUT_PREFIX .. "Could not find module: " .. moduleName, 2)
end
-- This is a hash which TS.import uses as a kind of linked-list-like history of [Script who Loaded] -> Library
local currentlyLoading = {}
local registeredLibraries = {}
function TS.import(context, module, ...)
for i = 1, select("#", ...) do
module = module:WaitForChild((select(i, ...)))
end
if module.ClassName ~= "ModuleScript" then
error(OUTPUT_PREFIX .. "Failed to import! Expected ModuleScript, got " .. module.ClassName, 2)
end
currentlyLoading[context] = module
-- Check to see if a case like this occurs:
-- module -> Module1 -> Module2 -> module
-- WHERE currentlyLoading[module] is Module1
-- and currentlyLoading[Module1] is Module2
-- and currentlyLoading[Module2] is module
local currentModule = module
local depth = 0
while currentModule do
depth = depth + 1
currentModule = currentlyLoading[currentModule]
if currentModule == module then
local str = currentModule.Name -- Get the string traceback
for _ = 1, depth do
currentModule = currentlyLoading[currentModule]
str = str .. "" .. currentModule.Name
end
error(OUTPUT_PREFIX .. "Failed to import! Detected a circular dependency chain: " .. str, 2)
end
end
if not registeredLibraries[module] then
if _G[module] then
error(
OUTPUT_PREFIX
.. "Invalid module access! Do you have multiple TS runtimes trying to import this? "
.. module:GetFullName(),
2
)
end
_G[module] = TS
registeredLibraries[module] = true -- register as already loaded for subsequent calls
end
local data = require(module)
if currentlyLoading[context] == module then -- Thread-safe cleanup!
currentlyLoading[context] = nil
end
return data
end
function TS.instanceof(obj, class)
-- custom Class.instanceof() check
if type(class) == "table" and type(class.instanceof) == "function" then
return class.instanceof(obj)
end
-- metatable check
if type(obj) == "table" then
obj = getmetatable(obj)
while obj ~= nil do
if obj == class then
return true
end
local mt = getmetatable(obj)
if mt then
obj = mt.__index
else
obj = nil
end
end
end
return false
end
function TS.async(callback)
return function(...)
local n = select("#", ...)
local args = { ... }
return Promise.new(function(resolve, reject)
coroutine.wrap(function()
local ok, result = pcall(callback, unpack(args, 1, n))
if ok then
resolve(result)
else
reject(result)
end
end)()
end)
end
end
function TS.await(promise)
if not Promise.is(promise) then
return promise
end
local status, value = promise:awaitStatus()
if status == Promise.Status.Resolved then
return value
elseif status == Promise.Status.Rejected then
error(value, 2)
else
error("The awaited Promise was cancelled", 2)
end
end
local SIGN = 2 ^ 31
local COMPLEMENT = 2 ^ 32
local function bit_sign(num)
-- Restores the sign after an unsigned conversion according to 2s complement.
if bit32.btest(num, SIGN) then
return num - COMPLEMENT
else
return num
end
end
function TS.bit_lrsh(a, b)
return bit_sign(bit32.arshift(a, b))
end
TS.TRY_RETURN = 1
TS.TRY_BREAK = 2
TS.TRY_CONTINUE = 3
function TS.try(try, catch, finally)
-- execute try
local trySuccess, exitTypeOrTryError, returns = pcall(try)
local exitType, tryError
if trySuccess then
exitType = exitTypeOrTryError
else
tryError = exitTypeOrTryError
end
local catchSuccess = true
local catchError
-- if try block failed, and catch block exists, execute catch
if not trySuccess and catch then
local newExitTypeOrCatchError, newReturns
catchSuccess, newExitTypeOrCatchError, newReturns = pcall(catch, tryError)
local newExitType
if catchSuccess then
newExitType = newExitTypeOrCatchError
else
catchError = newExitTypeOrCatchError
end
if newExitType then
exitType, returns = newExitType, newReturns
end
end
-- execute finally
if finally then
local newExitType, newReturns = finally()
if newExitType then
exitType, returns = newExitType, newReturns
end
end
-- if exit type is a control flow, do not rethrow errors
if exitType ~= TS.TRY_RETURN and exitType ~= TS.TRY_BREAK and exitType ~= TS.TRY_CONTINUE then
-- if catch block threw an error, rethrow it
if not catchSuccess then
error(catchError, 2)
end
-- if try block threw an error and there was no catch block, rethrow it
if not trySuccess and not catch then
error(tryError, 2)
end
end
return exitType, returns
end
function TS.generator(callback)
local co = coroutine.create(callback)
return {
next = function(...)
if coroutine.status(co) == "dead" then
return { done = true }
else
local success, value = coroutine.resume(co, ...)
if success == false then
error(value, 2)
end
return {
value = value,
done = coroutine.status(co) == "dead",
}
end
end,
}
end
return TS

2562
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "next_station_plugin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "rbxtsc",
"watch": "rbxtsc -w"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@rbxts/compiler-types": "^3.0.0-types.0",
"@rbxts/types": "^1.0.881",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-roblox-ts": "^1.1.0",
"prettier": "^3.6.2",
"roblox-ts": "^3.0.0",
"typescript": "^5.8.3"
}
}

16
serve.project.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "roblox-ts-plugin-serve",
"globIgnorePaths": [
"**/package.json",
"**/tsconfig.json"
],
"tree": {
"$className": "DataModel",
"ServerScriptService": {
"$className": "ServerScriptService",
"Plugin": {
"$path": "./default.project.json"
}
}
}
}

65
src/Adornment/Area.ts Normal file
View File

@@ -0,0 +1,65 @@
import { createAnchor } from "Utils/Gui";
import { Handles } from "./Handles";
function areaSphereAdornment(model: Folder, anchor: Part) {
const sphereAdornment = new Instance("SphereHandleAdornment");
sphereAdornment.Adornee = anchor;
sphereAdornment.AlwaysOnTop = true;
sphereAdornment.ZIndex = 5;
sphereAdornment.Color3 = new Color3(1, 0, 0);
sphereAdornment.Transparency = 0.8;
sphereAdornment.Radius = 0.4;
sphereAdornment.Parent = model;
return sphereAdornment;
}
export class HandlesArea {
originHandles: Handles;
originAdornment: SphereHandleAdornment;
originAnchor: Part;
originValueChanged;
tipHandles: Handles;
tipAdornment: SphereHandleAdornment;
tipAnchor: Part;
tipValueChanged;
box: BoxHandleAdornment;
boxAnchor: Part;
constructor(model: Folder, firstPos: Vector3, secondPos: Vector3) {
this.originHandles = new Handles(model, firstPos);
this.originAnchor = this.originHandles.anchor;
this.originAdornment = areaSphereAdornment(model, this.originHandles.anchor);
this.tipHandles = new Handles(model, secondPos);
this.tipAnchor = this.tipHandles.anchor;
this.tipAdornment = areaSphereAdornment(model, this.tipHandles.anchor);
// create the box
const boxSize = secondPos.sub(firstPos);
const boxCenter = firstPos.add(boxSize.div(2));
this.boxAnchor = createAnchor(model, boxCenter);
const box = new Instance("BoxHandleAdornment");
box.Adornee = this.boxAnchor;
box.AlwaysOnTop = true;
box.ZIndex = 5;
box.Color3 = new Color3(1, 0, 0);
box.Transparency = 0.8;
box.Size = new Vector3(math.abs(boxSize.X), math.abs(boxSize.Y), math.abs(boxSize.Z));
box.Parent = model;
this.box = box;
// Function
this.originAnchor.GetPropertyChangedSignal("Position").Connect(() => this.updateBox());
this.tipAnchor.GetPropertyChangedSignal("Position").Connect(() => this.updateBox());
this.originValueChanged = this.originHandles.valueChanged;
this.tipValueChanged = this.tipHandles.valueChanged;
}
updateBox() {
const origin = this.originAnchor.Position;
const tip = this.tipAnchor.Position;
const boxSize = tip.sub(origin);
const boxCenter = origin.add(boxSize.div(2));
this.boxAnchor.Position = boxCenter;
this.box.Size = new Vector3(math.abs(boxSize.X), math.abs(boxSize.Y), math.abs(boxSize.Z));
}
}

51
src/Adornment/Handles.ts Normal file
View File

@@ -0,0 +1,51 @@
import Signal from "@rbxts/signal";
import { createAnchor } from "Utils/Gui";
export class Handles {
currentVector: Vector3 = new Vector3();
anchor: Part;
valueChanged = new Signal<(newValue: Vector3) => void>();
constructor(model: Folder, startPosition: Vector3 = Vector3.zero, anchor = createAnchor(model, startPosition)) {
this.anchor = anchor;
const handles = new Instance("Handles");
handles.Adornee = this.anchor;
handles.Parent = model;
handles.MouseButton1Down.Connect(() => {
this.currentVector = this.anchor.Position;
});
handles.MouseDrag.Connect((face, distance) => {
// Apply the changes to the anchor's position
let offset = Vector3.zero;
switch (face) {
case Enum.NormalId.Right:
offset = new Vector3(distance, 0, 0);
break;
case Enum.NormalId.Left:
offset = new Vector3(-distance, 0, 0);
break;
case Enum.NormalId.Top:
offset = new Vector3(0, distance, 0);
break;
case Enum.NormalId.Bottom:
offset = new Vector3(0, -distance, 0);
break;
case Enum.NormalId.Front:
offset = new Vector3(0, 0, -distance);
break;
case Enum.NormalId.Back:
offset = new Vector3(0, 0, distance);
break;
}
this.anchor.Position = this.currentVector.add(offset);
});
handles.MouseButton1Up.Connect(() => {
const newpos = this.anchor.Position;
this.valueChanged.Fire(newpos);
});
}
}

View File

@@ -0,0 +1,13 @@
declare class CollapsibleTitledSection {
constructor(
nameSuffix: string,
titleText: string,
autoScalingList?: boolean,
minimizable?: boolean,
minimizedByDefault?: boolean,
);
GetSectionFrame(): Instance;
GetContentsFrame(): Instance;
SetCollapsedState(minimized: boolean): void;
}
export = CollapsibleTitledSection;

View File

@@ -0,0 +1,168 @@
----------------------------------------
--
-- CollapsibleTitledSectionClass
--
-- Creates a section with a title label:
--
-- "SectionXXX"
-- "TitleBarVisual"
-- "Contents"
--
-- Requires "parent" and "sectionName" parameters and returns the section and its contentsFrame
-- The entire frame will resize dynamically as contents frame changes size.
--
-- "autoScalingList" is a boolean that defines wheter or not the content frame automatically resizes when children are added.
-- This is important for cases when you want minimize button to push or contract what is below it.
--
-- Both "minimizeable" and "minimizedByDefault" are false by default
-- These parameters define if the section will have an arrow button infront of the title label,
-- which the user may use to hide the section's contents
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
local kRightButtonAsset = "rbxasset://textures/TerrainTools/button_arrow.png"
local kDownButtonAsset = "rbxasset://textures/TerrainTools/button_arrow_down.png"
local kArrowSize = 9
local kDoubleClickTimeSec = 0.5
CollapsibleTitledSectionClass = {}
CollapsibleTitledSectionClass.__index = CollapsibleTitledSectionClass
function CollapsibleTitledSectionClass.new(nameSuffix, titleText, autoScalingList, minimizable, minimizedByDefault)
local self = {}
setmetatable(self, CollapsibleTitledSectionClass)
self._minimized = minimizedByDefault
self._minimizable = minimizable
self._titleBarHeight = GuiUtilities.kTitleBarHeight
local frame = Instance.new('Frame')
frame.Name = 'CTSection' .. nameSuffix
frame.BackgroundTransparency = 1
self._frame = frame
local uiListLayout = Instance.new('UIListLayout')
uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder
uiListLayout.Parent = frame
self._uiListLayout = uiListLayout
local contentsFrame = Instance.new('Frame')
contentsFrame.Name = 'Contents'
contentsFrame.BackgroundTransparency = 1
contentsFrame.Size = UDim2.new(1, 0, 0, 1)
contentsFrame.Position = UDim2.new(0, 0, 0, titleBarSize)
contentsFrame.Parent = frame
contentsFrame.LayoutOrder = 2
GuiUtilities.syncGuiElementBackgroundColor(contentsFrame)
self._contentsFrame = contentsFrame
uiListLayout:GetPropertyChangedSignal('AbsoluteContentSize'):connect(function()
self:_UpdateSize()
end)
self:_UpdateSize()
self:_CreateTitleBar(titleText)
self:SetCollapsedState(self._minimized)
if (autoScalingList) then
GuiUtilities.MakeFrameAutoScalingList(self:GetContentsFrame())
end
return self
end
function CollapsibleTitledSectionClass:GetSectionFrame()
return self._frame
end
function CollapsibleTitledSectionClass:GetContentsFrame()
return self._contentsFrame
end
function CollapsibleTitledSectionClass:_UpdateSize()
local totalSize = self._uiListLayout.AbsoluteContentSize.Y
self._frame.Size = UDim2.new(1, 0, 0, totalSize)
end
function CollapsibleTitledSectionClass:_UpdateMinimizeButton()
-- We can't rotate it because rotated images don't get clipped by parents.
-- This is all in a scroll widget.
-- :(
if (self._minimized) then
self._minimizeButton.Image = kRightButtonAsset
else
self._minimizeButton.Image = kDownButtonAsset
end
end
function CollapsibleTitledSectionClass:SetCollapsedState(bool)
self._minimized = bool
self._contentsFrame.Visible = not bool
self:_UpdateMinimizeButton()
self:_UpdateSize()
end
function CollapsibleTitledSectionClass:_ToggleCollapsedState()
self:SetCollapsedState(not self._minimized)
end
function CollapsibleTitledSectionClass:_CreateTitleBar(titleText)
local titleTextOffset = self._titleBarHeight
local titleBar = Instance.new('ImageButton')
titleBar.AutoButtonColor = false
titleBar.Name = 'TitleBarVisual'
titleBar.BorderSizePixel = 0
titleBar.Position = UDim2.new(0, 0, 0, 0)
titleBar.Size = UDim2.new(1, 0, 0, self._titleBarHeight)
titleBar.Parent = self._frame
titleBar.LayoutOrder = 1
GuiUtilities.syncGuiElementTitleColor(titleBar)
local titleLabel = Instance.new('TextLabel')
titleLabel.Name = 'TitleLabel'
titleLabel.BackgroundTransparency = 1
titleLabel.Font = Enum.Font.SourceSansBold --todo: input spec font
titleLabel.TextSize = 15 --todo: input spec font size
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
titleLabel.Text = titleText
titleLabel.Position = UDim2.new(0, titleTextOffset, 0, 0)
titleLabel.Size = UDim2.new(1, -titleTextOffset, 1, GuiUtilities.kTextVerticalFudge)
titleLabel.Parent = titleBar
GuiUtilities.syncGuiElementFontColor(titleLabel)
self._minimizeButton = Instance.new('ImageButton')
self._minimizeButton.Name = 'MinimizeSectionButton'
self._minimizeButton.Image = kRightButtonAsset --todo: input arrow image from spec
self._minimizeButton.Size = UDim2.new(0, kArrowSize, 0, kArrowSize)
self._minimizeButton.AnchorPoint = Vector2.new(0.5, 0.5)
self._minimizeButton.Position = UDim2.new(0, self._titleBarHeight*.5,
0, self._titleBarHeight*.5)
self._minimizeButton.BackgroundTransparency = 1
self._minimizeButton.Visible = self._minimizable -- only show when minimizable
self._minimizeButton.MouseButton1Down:connect(function()
self:_ToggleCollapsedState()
end)
self:_UpdateMinimizeButton()
self._minimizeButton.Parent = titleBar
self._latestClickTime = 0
titleBar.MouseButton1Down:connect(function()
local now = tick()
if (now - self._latestClickTime < kDoubleClickTimeSec) then
self:_ToggleCollapsedState()
self._latestClickTime = 0
else
self._latestClickTime = now
end
end)
end
return CollapsibleTitledSectionClass

5
src/Lualibs/CustomTextButton.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare class CustomTextButtonClass {
constructor(buttonName: string, labelText: string);
GetButton(): Instance;
}
export = CustomTextButtonClass;

View File

@@ -0,0 +1,94 @@
----------------------------------------
--
-- CustomTextButton.lua
--
-- Creates text button with custom look & feel, hover/click effects.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
local kButtonImageIdDefault = "rbxasset://textures/TerrainTools/button_default.png"
local kButtonImageIdHovered = "rbxasset://textures/TerrainTools/button_hover.png"
local kButtonImageIdPressed = "rbxasset://textures/TerrainTools/button_pressed.png"
CustomTextButtonClass = {}
CustomTextButtonClass.__index = CustomTextButtonClass
function CustomTextButtonClass.new(buttonName, labelText)
local self = {}
setmetatable(self, CustomTextButtonClass)
local button = Instance.new('ImageButton')
button.Name = buttonName
button.Image = kButtonImageIdDefault
button.BackgroundTransparency = 1
button.ScaleType = Enum.ScaleType.Slice
button.SliceCenter = Rect.new(7, 7, 156, 36)
button.AutoButtonColor = false
local label = Instance.new('TextLabel')
label.Text = labelText
label.BackgroundTransparency = 1
label.Size = UDim2.new(1, 0, 1, GuiUtilities.kButtonVerticalFudge)
label.Font = Enum.Font.SourceSans
label.TextSize = 15
label.Parent = button
self._label = label
self._button = button
self._clicked = false
self._hovered = false
button.InputBegan:connect(function(input)
if (input.UserInputType == Enum.UserInputType.MouseMovement) then
self._hovered = true
self:_updateButtonVisual()
end
end)
button.InputEnded:connect(function(input)
if (input.UserInputType == Enum.UserInputType.MouseMovement) then
self._hovered = false
self._clicked = false
self:_updateButtonVisual()
end
end)
button.MouseButton1Down:connect(function()
self._clicked = true
self:_updateButtonVisual()
end)
button.MouseButton1Up:connect(function()
self._clicked = false
self:_updateButtonVisual()
end)
self:_updateButtonVisual()
return self
end
function CustomTextButtonClass:_updateButtonVisual()
if (self._clicked) then
self._button.Image = kButtonImageIdPressed
self._label.TextColor3 = GuiUtilities.kPressedButtonTextColor
elseif (self._hovered) then
self._button.Image = kButtonImageIdHovered
self._label.TextColor3 = GuiUtilities.kStandardButtonTextColor
else
self._button.Image = kButtonImageIdDefault
self._label.TextColor3 = GuiUtilities.kStandardButtonTextColor
end
end
-- Backwards compatibility (should be removed in the future)
-- CustomTextButtonClass.getButton = CustomTextButtonClass.GetButton
function CustomTextButtonClass:GetButton()
return self._button
end
return CustomTextButtonClass

View File

@@ -0,0 +1,247 @@
local module = {}
module.kTitleBarHeight = 27
module.kInlineTitleBarHeight = 24
module.kStandardContentAreaWidth = 180
module.kStandardPropertyHeight = 30
module.kSubSectionLabelHeight = 30
module.kStandardVMargin = 7
module.kStandardHMargin = 16
module.kCheckboxMinLabelWidth = 52
module.kCheckboxMinMargin = 12
module.kCheckboxWidth = 12
module.kRadioButtonsHPadding = 24
module.StandardLineLabelLeftMargin = module.kTitleBarHeight
module.StandardLineElementLeftMargin = (module.StandardLineLabelLeftMargin + module.kCheckboxMinLabelWidth
+ module.kCheckboxMinMargin + module.kCheckboxWidth + module.kRadioButtonsHPadding)
module.StandardLineLabelWidth = (module.StandardLineElementLeftMargin - module.StandardLineLabelLeftMargin - 10 )
module.kDropDownHeight = 55
module.kBottomButtonsFrameHeight = 50
module.kBottomButtonsHeight = 28
module.kShapeButtonSize = 32
module.kTextVerticalFudge = -3
module.kButtonVerticalFudge = -5
module.kBottomButtonsWidth = 100
module.kDisabledTextColor = Color3.new(.4, .4, .4) --todo: input spec disabled text color
module.kStandardButtonTextColor = Color3.new(0, 0, 0) --todo: input spec disabled text color
module.kPressedButtonTextColor = Color3.new(1, 1, 1) --todo: input spec disabled text color
module.kButtonStandardBackgroundColor = Color3.new(1, 1, 1) --todo: sync with spec
module.kButtonStandardBorderColor = Color3.new(.4,.4,.4) --todo: sync with spec
module.kButtonDisabledBackgroundColor = Color3.new(.7,.7,.7) --todo: sync with spec
module.kButtonDisabledBorderColor = Color3.new(.6,.6,.6) --todo: sync with spec
module.kButtonBackgroundTransparency = 0.5
module.kButtonBackgroundIntenseTransparency = 0.4
module.kMainFrame = nil
function module.ShouldUseIconsForDarkerBackgrounds()
local mainColor = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
return (mainColor.r + mainColor.g + mainColor.b) / 3 < 0.5
end
function module.SetMainFrame(frame)
module.kMainFrame = frame
end
function module.syncGuiElementTitleColor(guiElement)
local function setColors()
guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Titlebar)
end
settings().Studio.ThemeChanged:connect(setColors)
setColors()
end
function module.syncGuiElementInputFieldColor(guiElement)
local function setColors()
guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground)
end
settings().Studio.ThemeChanged:connect(setColors)
setColors()
end
function module.syncGuiElementBackgroundColor(guiElement)
local function setColors()
guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
end
settings().Studio.ThemeChanged:connect(setColors)
setColors()
end
function module.syncGuiElementStripeColor(guiElement)
local function setColors()
if ((guiElement.LayoutOrder + 1) % 2 == 0) then
guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
else
guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.CategoryItem)
end
end
settings().Studio.ThemeChanged:connect(setColors)
setColors()
end
function module.syncGuiElementBorderColor(guiElement)
local function setColors()
guiElement.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border)
end
settings().Studio.ThemeChanged:connect(setColors)
setColors()
end
function module.syncGuiElementFontColor(guiElement)
local function setColors()
guiElement.TextColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainText)
end
settings().Studio.ThemeChanged:connect(setColors)
setColors()
end
function module.syncGuiElementScrollColor(guiElement)
local function setColors()
guiElement.ScrollBarImageColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar)
end
settings().Studio.ThemeChanged:connect(setColors)
setColors()
end
-- A frame with standard styling.
function module.MakeFrame(name)
local frame = Instance.new("Frame")
frame.Name = name
frame.BackgroundTransparency = 0
frame.BorderSizePixel = 0
module.syncGuiElementBackgroundColor(frame)
return frame
end
-- A frame that is a whole line, containing some arbitrary sized widget.
function module.MakeFixedHeightFrame(name, height)
local frame = module.MakeFrame(name)
frame.Size = UDim2.new(1, 0, 0, height)
return frame
end
-- A frame that is one standard-sized line, containing some standard-sized widget (label, edit box, dropdown,
-- checkbox)
function module.MakeStandardFixedHeightFrame(name)
return module.MakeFixedHeightFrame(name, module.kStandardPropertyHeight)
end
function module.AdjustHeightDynamicallyToLayout(frame, uiLayout, optPadding)
if (not optPadding) then
optPadding = 0
end
local function updateSizes()
frame.Size = UDim2.new(1, 0, 0, uiLayout.AbsoluteContentSize.Y + optPadding)
end
uiLayout:GetPropertyChangedSignal("AbsoluteContentSize"):connect(updateSizes)
updateSizes()
end
-- Assumes input frame has a List layout with sort order layout order.
-- Add frames in order as siblings of list layout, they will be laid out in order.
-- Color frame background accordingly.
function module.AddStripedChildrenToListFrame(listFrame, frames)
for index, frame in ipairs(frames) do
frame.Parent = listFrame
frame.LayoutOrder = index
frame.BackgroundTransparency = 0
frame.BorderSizePixel = 1
module.syncGuiElementStripeColor(frame)
module.syncGuiElementBorderColor(frame)
end
end
local function MakeSectionInternal(parentGui, name, title, contentHeight)
local frame = Instance.new("Frame")
frame.Name = name
frame.BackgroundTransparency = 1
frame.Parent = parentGui
frame.BackgroundTransparency = 1
frame.BorderSizePixel = 0
-- If title is "nil', no title bar.
local contentYOffset = 0
local titleBar = nil
if (title ~= nil) then
local titleBarFrame = Instance.new("Frame")
titleBarFrame.Name = "TitleBarFrame"
titleBarFrame.Parent = frame
titleBarFrame.Position = UDim2.new(0, 0, 0, 0)
titleBarFrame.LayoutOrder = 0
local titleBar = Instance.new("TextLabel")
titleBar.Name = "TitleBarLabel"
titleBar.Text = title
titleBar.Parent = titleBarFrame
titleBar.BackgroundTransparency = 1
titleBar.Position = UDim2.new(0, module.kStandardHMargin, 0, 0)
module.syncGuiElementFontColor(titleBar)
contentYOffset = contentYOffset + module.kTitleBarHeight
end
frame.Size = UDim2.new(1, 0, 0, contentYOffset + contentHeight)
return frame
end
function module.MakeStandardPropertyLabel(text, opt_ignoreThemeUpdates)
local label = Instance.new('TextLabel')
label.Name = 'Label'
label.BackgroundTransparency = 1
label.Font = Enum.Font.SourceSans --todo: input spec font
label.TextSize = 15 --todo: input spec font size
label.TextXAlignment = Enum.TextXAlignment.Left
label.Text = text
label.AnchorPoint = Vector2.new(0, 0.5)
label.Position = UDim2.new(0, module.StandardLineLabelLeftMargin, 0.5, module.kTextVerticalFudge)
label.Size = UDim2.new(0, module.StandardLineLabelWidth, 1, 0)
if (not opt_ignoreThemeUpdates) then
module.syncGuiElementFontColor(label)
end
return label
end
function module.MakeFrameWithSubSectionLabel(name, text)
local row = module.MakeFixedHeightFrame(name, module.kSubSectionLabelHeight)
row.BackgroundTransparency = 1
local label = module.MakeStandardPropertyLabel(text)
label.BackgroundTransparency = 1
label.Parent = row
return row
end
function module.MakeFrameAutoScalingList(frame)
local uiListLayout = Instance.new("UIListLayout")
uiListLayout.Parent = frame
uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder
module.AdjustHeightDynamicallyToLayout(frame, uiListLayout)
end
return module

17
src/Lualibs/ImageButtonWithText.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
declare class ImageButtonWithTextClass {
constructor(
name: string,
layoutOrder: number,
icon: string,
text: string,
buttonSize: UDim2,
imageSize: UDim2,
imagePos: UDim2,
textSize: UDim2,
textPos: UDim2,
);
GetButton(): Instance;
SetSelected(selected: boolean): void;
GetSelected(): boolean;
}
export = ImageButtonWithTextClass;

View File

@@ -0,0 +1,158 @@
----------------------------------------
--
-- ImageButtonWithText.lua
--
-- An image button with text underneath. Standardized hover, clicked, and
-- selected states.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
ImageButtonWithTextClass = {}
ImageButtonWithTextClass.__index = ImageButtonWithTextClass
local kSelectedBaseTransparency = 0.2
local kAdditionalTransparency = 0.1
function ImageButtonWithTextClass.new(name,
layoutOrder,
icon,
text,
buttonSize,
imageSize,
imagePos,
textSize,
textPos)
local self = {}
setmetatable(self, ImageButtonWithTextClass)
local button = Instance.new("ImageButton")
button.Name = name
button.AutoButtonColor = false
button.Size = buttonSize
button.BorderSizePixel = 1
-- Image-with-text button has translucent background and "selected" background color.
-- When selected we set transluency to not-zero so we see selected color.
button.BackgroundTransparency = 1
button.LayoutOrder = layoutOrder
local buttonIcon = Instance.new("ImageLabel")
buttonIcon.BackgroundTransparency = 1
buttonIcon.Image = icon or ""
buttonIcon.Size = imageSize
buttonIcon.Position = imagePos
buttonIcon.Parent = button
local textLabel = Instance.new("TextLabel")
textLabel.BackgroundTransparency = 1
textLabel.Text = text
textLabel.Size = textSize
textLabel.Position = textPos
textLabel.TextScaled = true
textLabel.Font = Enum.Font.SourceSans
textLabel.Parent = button
GuiUtilities.syncGuiElementFontColor(textLabel)
local uiTextSizeConstraint = Instance.new("UITextSizeConstraint")
-- Spec asks for fontsize of 12 pixels, but in Roblox the text font sizes look smaller than the mock
--Note: For this font the Roblox text size is 25.7% larger than the design spec.
uiTextSizeConstraint.MaxTextSize = 15
uiTextSizeConstraint.Parent = textLabel
self._button = button
self._clicked = false
self._hovered = false
self._selected = false
button.InputBegan:Connect(function(input)
if (input.UserInputType == Enum.UserInputType.MouseMovement) then
self._hovered = true
self:_updateButtonVisual()
end
end)
button.InputEnded:Connect(function(input)
if (input.UserInputType == Enum.UserInputType.MouseMovement) then
self._hovered = false
self._clicked = false
self:_updateButtonVisual()
end
end)
button.MouseButton1Down:Connect(function()
self._clicked = true
self:_updateButtonVisual()
end)
button.MouseButton1Up:Connect(function()
self._clicked = false
self:_updateButtonVisual()
end)
function updateButtonVisual()
self:_updateButtonVisual()
end
settings().Studio.ThemeChanged:connect(updateButtonVisual)
self:_updateButtonVisual()
return self
end
function ImageButtonWithTextClass:_updateButtonVisual()
-- Possibilties:
if (self._clicked) then
-- This covers 'clicked and selected' or 'clicked'
self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button,
Enum.StudioStyleGuideModifier.Selected)
self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border,
Enum.StudioStyleGuideModifier.Selected)
if (self._selected) then
self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundIntenseTransparency
else
self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundTransparency
end
elseif (self._hovered) then
-- This covers 'hovered and selected' or 'hovered'
self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button,
Enum.StudioStyleGuideModifier.Hover)
self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border,
Enum.StudioStyleGuideModifier.Hover)
if (self._selected) then
self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundIntenseTransparency
else
self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundTransparency
end
elseif (self._selected) then
-- This covers 'selected'
self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button,
Enum.StudioStyleGuideModifier.Selected)
self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border,
Enum.StudioStyleGuideModifier.Selected)
self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundTransparency
else
-- This covers 'no special state'
self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button)
self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border)
self._button.BackgroundTransparency = 1
end
end
function ImageButtonWithTextClass:GetButton()
return self._button
end
function ImageButtonWithTextClass:SetSelected(selected)
self._selected = selected
self:_updateButtonVisual()
end
function ImageButtonWithTextClass:GetSelected()
return self._selected
end
return ImageButtonWithTextClass

14
src/Lualibs/LabeledCheckbox.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
declare class LabeledCheckboxClass {
constructor(nameSuffix: string, labelText: string, initValue?: boolean, initDisabled?: boolean);
GetFrame(): Instance;
GetValue(): boolean;
GetLabel(): Instance;
GetButton(): Instance;
SetValueChangedFunction(fn: (value: boolean) => void): void;
SetDisabled(disabled: boolean): void;
GetDisabled(): boolean;
SetValue(value: boolean): void;
UseSmallSize(): void;
DisableWithOverrideValue(overrideValue: boolean): void;
}
export = LabeledCheckboxClass;

View File

@@ -0,0 +1,243 @@
----------------------------------------
--
-- LabeledCheckbox.lua
--
-- Creates a frame containing a label and a checkbox.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
local kCheckboxWidth = GuiUtilities.kCheckboxWidth
local kMinTextSize = 14
local kMinHeight = 24
local kMinLabelWidth = GuiUtilities.kCheckboxMinLabelWidth
local kMinMargin = GuiUtilities.kCheckboxMinMargin
local kMinButtonWidth = kCheckboxWidth;
local kMinLabelSize = UDim2.new(0, kMinLabelWidth, 0, kMinHeight)
local kMinLabelPos = UDim2.new(0, kMinButtonWidth + kMinMargin, 0, kMinHeight/2)
local kMinButtonSize = UDim2.new(0, kMinButtonWidth, 0, kMinButtonWidth)
local kMinButtonPos = UDim2.new(0, 0, 0, kMinHeight/2)
local kCheckImageWidth = 8
local kMinCheckImageWidth = kCheckImageWidth
local kCheckImageSize = UDim2.new(0, kCheckImageWidth, 0, kCheckImageWidth)
local kMinCheckImageSize = UDim2.new(0, kMinCheckImageWidth, 0, kMinCheckImageWidth)
local kEnabledCheckImage = "rbxasset://textures/TerrainTools/icon_tick.png"
local kDisabledCheckImage = "rbxasset://textures/TerrainTools/icon_tick_grey.png"
local kCheckboxFrameImage = "rbxasset://textures/TerrainTools/checkbox_square.png"
LabeledCheckboxClass = {}
LabeledCheckboxClass.__index = LabeledCheckboxClass
LabeledCheckboxClass.kMinFrameSize = UDim2.new(0, kMinLabelWidth + kMinMargin + kMinButtonWidth, 0, kMinHeight)
function LabeledCheckboxClass.new(nameSuffix, labelText, initValue, initDisabled)
local self = {}
setmetatable(self, LabeledCheckboxClass)
local initValue = not not initValue
local initDisabled = not not initDisabled
local frame = GuiUtilities.MakeStandardFixedHeightFrame("CBF" .. nameSuffix)
local fullBackgroundButton = Instance.new("TextButton")
fullBackgroundButton.Name = "FullBackground"
fullBackgroundButton.Parent = frame
fullBackgroundButton.BackgroundTransparency = 1
fullBackgroundButton.Size = UDim2.new(1, 0, 1, 0)
fullBackgroundButton.Position = UDim2.new(0, 0, 0, 0)
fullBackgroundButton.Text = ""
local label = GuiUtilities.MakeStandardPropertyLabel(labelText, true)
label.Parent = fullBackgroundButton
local button = Instance.new('ImageButton')
button.Name = 'Button'
button.Size = UDim2.new(0, kCheckboxWidth, 0, kCheckboxWidth)
button.AnchorPoint = Vector2.new(0, .5)
button.BackgroundTransparency = 0
button.Position = UDim2.new(0, GuiUtilities.StandardLineElementLeftMargin, .5, 0)
button.Parent = fullBackgroundButton
button.Image = kCheckboxFrameImage
button.BorderSizePixel = 0
button.AutoButtonColor = false
local checkImage = Instance.new("ImageLabel")
checkImage.Name = "CheckImage"
checkImage.Parent = button
checkImage.Image = kEnabledCheckImage
checkImage.Visible = false
checkImage.Size = kCheckImageSize
checkImage.AnchorPoint = Vector2.new(0.5, 0.5)
checkImage.Position = UDim2.new(0.5, 0, 0.5, 0)
checkImage.BackgroundTransparency = 1
checkImage.BorderSizePixel = 0
self._frame = frame
self._button = button
self._label = label
self._checkImage = checkImage
self._fullBackgroundButton = fullBackgroundButton
self._useDisabledOverride = false
self._disabledOverride = false
self:SetDisabled(initDisabled)
self._value = not initValue
self:SetValue(initValue)
self:_SetupMouseClickHandling()
local function updateFontColors()
self:UpdateFontColors()
end
settings().Studio.ThemeChanged:connect(updateFontColors)
updateFontColors()
return self
end
function LabeledCheckboxClass:_MaybeToggleState()
if not self._disabled then
self:SetValue(not self._value)
end
end
function LabeledCheckboxClass:_SetupMouseClickHandling()
self._button.MouseButton1Down:connect(function()
self:_MaybeToggleState()
end)
self._fullBackgroundButton.MouseButton1Down:connect(function()
self:_MaybeToggleState()
end)
end
function LabeledCheckboxClass:_HandleUpdatedValue()
self._checkImage.Visible = self:GetValue()
if (self._valueChangedFunction) then
self._valueChangedFunction(self:GetValue())
end
end
-- Small checkboxes are a different entity.
-- All the bits are smaller.
-- Fixed width instead of flood-fill.
-- Box comes first, then label.
function LabeledCheckboxClass:UseSmallSize()
self._label.TextSize = kMinTextSize
self._label.Size = kMinLabelSize
self._label.Position = kMinLabelPos
self._label.TextXAlignment = Enum.TextXAlignment.Left
self._button.Size = kMinButtonSize
self._button.Position = kMinButtonPos
self._checkImage.Size = kMinCheckImageSize
self._frame.Size = LabeledCheckboxClass.kMinFrameSize
self._frame.BackgroundTransparency = 1
end
function LabeledCheckboxClass:GetFrame()
return self._frame
end
function LabeledCheckboxClass:GetValue()
-- If button is disabled, and we should be using a disabled override,
-- use the disabled override.
if (self._disabled and self._useDisabledOverride) then
return self._disabledOverride
else
return self._value
end
end
function LabeledCheckboxClass:GetLabel()
return self._label
end
function LabeledCheckboxClass:GetButton()
return self._button
end
function LabeledCheckboxClass:SetValueChangedFunction(vcFunction)
self._valueChangedFunction = vcFunction
end
function LabeledCheckboxClass:SetDisabled(newDisabled)
local newDisabled = not not newDisabled
local originalValue = self:GetValue()
if newDisabled ~= self._disabled then
self._disabled = newDisabled
-- if we are no longer disabled, then we don't need or want
-- the override any more. Forget it.
if (not self._disabled) then
self._useDisabledOverride = false
end
if (newDisabled) then
self._checkImage.Image = kDisabledCheckImage
else
self._checkImage.Image = kEnabledCheckImage
end
self:UpdateFontColors()
self._button.BackgroundColor3 = self._disabled and GuiUtilities.kButtonDisabledBackgroundColor or GuiUtilities.kButtonStandardBackgroundColor
self._button.BorderColor3 = self._disabled and GuiUtilities.kButtonDisabledBorderColor or GuiUtilities.kButtonStandardBorderColor
if self._disabledChangedFunction then
self._disabledChangedFunction(self._disabled)
end
end
local newValue = self:GetValue()
if (newValue ~= originalValue) then
self:_HandleUpdatedValue()
end
end
function LabeledCheckboxClass:UpdateFontColors()
if self._disabled then
self._label.TextColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.DimmedText)
else
self._label.TextColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainText)
end
end
function LabeledCheckboxClass:DisableWithOverrideValue(overrideValue)
-- Disable this checkbox. While disabled, force value to override
-- value.
local oldValue = self:GetValue()
self._useDisabledOverride = true
self._disabledOverride = overrideValue
self:SetDisabled(true)
local newValue = self:GetValue()
if (oldValue ~= newValue) then
self:_HandleUpdatedValue()
end
end
function LabeledCheckboxClass:GetDisabled()
return self._disabled
end
function LabeledCheckboxClass:SetValue(newValue)
local newValue = not not newValue
if newValue ~= self._value then
self._value = newValue
self:_HandleUpdatedValue()
end
end
return LabeledCheckboxClass

12
src/Lualibs/LabeledMultiChoice.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
interface LabeledMultiChoiceChoice {
Id: string;
Text: string;
}
declare class LabeledMultiChoiceClass {
constructor(nameSuffix: string, labelText: string, choices: LabeledMultiChoiceChoice[], initChoiceIndex?: number);
SetSelectedIndex(index: number): void;
GetSelectedIndex(): number;
SetValueChangedFunction(fn: (index: number) => void): void;
GetFrame(): Instance;
}
export = LabeledMultiChoiceClass;

View File

@@ -0,0 +1,132 @@
----------------------------------------
--
-- LabeledMultiChoice.lua
--
-- Creates a frame containing a label and list of choices, of which exactly one
-- is always selected.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
LabeledRadioButton = require(script.Parent.LabeledRadioButton)
LabeledCheckbox = require(script.Parent.LabeledCheckbox)
VerticallyScalingListFrame = require(script.Parent.VerticallyScalingListFrame)
local kRadioButtonsHPadding = GuiUtilities.kRadioButtonsHPadding
LabeledMultiChoiceClass = {}
LabeledMultiChoiceClass.__index = LabeledMultiChoiceClass
-- Note:
-- "choices" is an array of entries.
-- each entry must have at least 2 fields:
-- "Id" - a unique (in the scope of choices) string id. Not visible to user.
-- "Text" - user-facing string: the label for the choice.
function LabeledMultiChoiceClass.new(nameSuffix, labelText, choices, initChoiceIndex)
local self = {}
setmetatable(self, LabeledMultiChoiceClass)
self._buttonObjsByIndex = {}
if (not initChoiceIndex ) then
initChoiceIndex = 1
end
if (initChoiceIndex > #choices) then
initChoiceIndex = #choices
end
local vsl = VerticallyScalingListFrame.new("MCC_" .. nameSuffix)
vsl:AddBottomPadding()
local titleLabel = GuiUtilities.MakeFrameWithSubSectionLabel("Title", labelText)
vsl:AddChild(titleLabel)
-- Container for cells.
local cellFrame = self:_MakeRadioButtons(choices)
vsl:AddChild(cellFrame)
self._vsl = vsl
self:SetSelectedIndex(initChoiceIndex)
return self
end
function LabeledMultiChoiceClass:SetSelectedIndex(selectedIndex)
self._selectedIndex = selectedIndex
for i = 1, #self._buttonObjsByIndex do
self._buttonObjsByIndex[i]:SetValue(i == selectedIndex)
end
if (self._valueChangedFunction) then
self._valueChangedFunction(self._selectedIndex)
end
end
function LabeledMultiChoiceClass:GetSelectedIndex()
return self._selectedIndex
end
function LabeledMultiChoiceClass:SetValueChangedFunction(vcf)
self._valueChangedFunction = vcf
end
function LabeledMultiChoiceClass:GetFrame()
return self._vsl:GetFrame()
end
-- Small checkboxes are a different entity.
-- All the bits are smaller.
-- Fixed width instead of flood-fill.
-- Box comes first, then label.
function LabeledMultiChoiceClass:_MakeRadioButtons(choices)
local frame = GuiUtilities.MakeFrame("RadioButtons")
frame.BackgroundTransparency = 1
local padding = Instance.new("UIPadding")
padding.PaddingLeft = UDim.new(0, GuiUtilities.StandardLineLabelLeftMargin)
padding.PaddingRight = UDim.new(0, GuiUtilities.StandardLineLabelLeftMargin)
padding.Parent = frame
-- Make a grid to put checkboxes in.
local uiGridLayout = Instance.new("UIGridLayout")
uiGridLayout.CellSize = LabeledCheckbox.kMinFrameSize
uiGridLayout.CellPadding = UDim2.new(0,
kRadioButtonsHPadding,
0,
GuiUtilities.kStandardVMargin)
uiGridLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left
uiGridLayout.VerticalAlignment = Enum.VerticalAlignment.Top
uiGridLayout.Parent = frame
uiGridLayout.SortOrder = Enum.SortOrder.LayoutOrder
for i, choiceData in ipairs(choices) do
self:_AddRadioButton(frame, i, choiceData)
end
-- Sync size with content size.
GuiUtilities.AdjustHeightDynamicallyToLayout(frame, uiGridLayout)
return frame
end
function LabeledMultiChoiceClass:_AddRadioButton(parentFrame, index, choiceData)
local radioButtonObj = LabeledRadioButton.new(choiceData.Id, choiceData.Text)
self._buttonObjsByIndex[index] = radioButtonObj
radioButtonObj:SetValueChangedFunction(function(value)
-- If we notice the button going from off to on, and it disagrees with
-- our current notion of selection, update selection.
if (value and self._selectedIndex ~= index) then
self:SetSelectedIndex(index)
end
end)
radioButtonObj:GetFrame().LayoutOrder = index
radioButtonObj:GetFrame().Parent = parentFrame
end
return LabeledMultiChoiceClass

8
src/Lualibs/LabeledRadioButton.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
declare class LabeledRadioButtonClass {
constructor(nameSuffix: string, labelText: string);
GetFrame(): Instance;
GetValue(): boolean;
SetValueChangedFunction(fn: (value: boolean) => void): void;
SetValue(value: boolean): void;
}
export = LabeledRadioButtonClass;

View File

@@ -0,0 +1,60 @@
----------------------------------------
--
-- LabeledRadioButton.lua
--
-- Creates a frame containing a label and a radio button.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
LabeledCheckbox = require(script.Parent.LabeledCheckbox)
local kButtonImage = "rbxasset://textures/TerrainTools/radio_button_frame.png"
local kBulletImage = "rbxasset://textures/TerrainTools/radio_button_bullet.png"
local kButtonImageDark = "rbxasset://textures/TerrainTools/radio_button_frame_dark.png"
local kBulletImageDark = "rbxasset://textures/TerrainTools/radio_button_bullet_dark.png"
local kFrameSize = 12
local kBulletSize = 14
LabeledRadioButtonClass = {}
LabeledRadioButtonClass.__index = LabeledRadioButtonClass
setmetatable(LabeledRadioButtonClass, LabeledCheckbox)
function LabeledRadioButtonClass.new(nameSuffix, labelText)
local newButton = LabeledCheckbox.new(nameSuffix, labelText, false)
setmetatable(newButton, LabeledRadioButtonClass)
newButton:UseSmallSize()
newButton._checkImage.Position = UDim2.new(0.5, 0, 0.5, 0)
newButton._checkImage.Image = kBulletImage
newButton._checkImage.Size = UDim2.new(0, kBulletSize, 0, kBulletSize)
newButton._button.Image = kButtonImage
newButton._button.Size = UDim2.new(0, kFrameSize, 0, kFrameSize)
newButton._button.BackgroundTransparency = 1
local function updateImages()
if (GuiUtilities:ShouldUseIconsForDarkerBackgrounds()) then
newButton._checkImage.Image = kBulletImageDark
newButton._button.Image = kButtonImageDark
else
newButton._checkImage.Image = kBulletImage
newButton._button.Image = kButtonImage
end
end
settings().Studio.ThemeChanged:connect(updateImages)
updateImages()
return newButton
end
function LabeledRadioButtonClass:_MaybeToggleState()
-- A checkbox can never be toggled off.
-- Only turns off because another one turns on.
if (not self._disabled and not self._value) then
self:SetValue(not self._value)
end
end
return LabeledRadioButtonClass

8
src/Lualibs/LabeledSlider.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
declare class LabeledSliderClass {
constructor(nameSuffix: string, labelText: string, sliderIntervals?: number, defaultValue?: number);
SetValueChangedFunction(fn: (value: number) => void): void;
GetFrame(): Instance;
SetValue(value: number): void;
GetValue(): number;
}
export = LabeledSliderClass;

View File

@@ -0,0 +1,122 @@
----------------------------------------
--
-- LabeledSlider.lua
--
-- Creates a frame containing a label and a slider control.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
rbxGuiLibrary = require(script.Parent.RbxGui)
local kSliderWidth = 100
local kSliderThumbImage = "rbxasset://textures/TerrainTools/sliderbar_button.png"
local kPreThumbImage = "rbxasset://textures/TerrainTools/sliderbar_blue.png"
local kPostThumbImage = "rbxasset://textures/TerrainTools/sliderbar_grey.png"
local kThumbSize = 13
local kSteps = 100
LabeledSliderClass = {}
LabeledSliderClass.__index = LabeledSliderClass
function LabeledSliderClass.new(nameSuffix, labelText, sliderIntervals, defaultValue)
local self = {}
setmetatable(self, LabeledSliderClass)
self._valueChangedFunction = nil
local sliderIntervals = sliderIntervals or 100
local defaultValue = defaultValue or 1
local frame = GuiUtilities.MakeStandardFixedHeightFrame('Slider' .. nameSuffix)
self._frame = frame
local label = GuiUtilities.MakeStandardPropertyLabel(labelText)
label.Parent = frame
self._label = label
self._value = defaultValue
--steps, width, position
local slider, sliderValue = rbxGuiLibrary.CreateSlider(sliderIntervals,
kSteps,
UDim2.new(0, 0, .5, -3))
self._slider = slider
self._sliderValue = sliderValue
-- Some tweaks to make slider look nice.
-- Hide the existing bar.
slider.Bar.BackgroundTransparency = 1
-- Replace slider thumb image.
self._thumb = slider.Bar.Slider
self._thumb.Image = kSliderThumbImage
self._thumb.AnchorPoint = Vector2.new(0.5, 0.5)
self._thumb.Size = UDim2.new(0, kThumbSize, 0, kThumbSize)
-- Add images on bar.
self._preThumbImage = Instance.new("ImageLabel")
self._preThumbImage.Name = "PreThumb"
self._preThumbImage.Parent = slider.Bar
self._preThumbImage.Size = UDim2.new(1, 0, 1, 0)
self._preThumbImage.Position = UDim2.new(0, 0, 0, 0)
self._preThumbImage.Image = kPreThumbImage
self._preThumbImage.BorderSizePixel = 0
self._postThumbImage = Instance.new("ImageLabel")
self._postThumbImage.Name = "PostThumb"
self._postThumbImage.Parent = slider.Bar
self._postThumbImage.Size = UDim2.new(1, 0, 1, 0)
self._postThumbImage.Position = UDim2.new(0, 0, 0, 0)
self._postThumbImage.Image = kPostThumbImage
self._postThumbImage.BorderSizePixel = 0
sliderValue.Changed:connect(function()
self._value = sliderValue.Value
-- Min value is 1.
-- Max value is sliderIntervals.
-- So scale is...
local scale = (self._value - 1)/(sliderIntervals-1)
self._preThumbImage.Size = UDim2.new(scale, 0, 1, 0)
self._postThumbImage.Size = UDim2.new(1 - scale, 0, 1, 0)
self._postThumbImage.Position = UDim2.new(scale, 0, 0, 0)
self._thumb.Position = UDim2.new(scale, 0,
0.5, 0)
if self._valueChangedFunction then
self._valueChangedFunction(self._value)
end
end)
self:SetValue(defaultValue)
slider.AnchorPoint = Vector2.new(0, 0.5)
slider.Size = UDim2.new(0, kSliderWidth, 1, 0)
slider.Position = UDim2.new(0, GuiUtilities.StandardLineElementLeftMargin, 0, GuiUtilities.kStandardPropertyHeight/2)
slider.Parent = frame
return self
end
function LabeledSliderClass:SetValueChangedFunction(vcf)
self._valueChangedFunction = vcf
end
function LabeledSliderClass:GetFrame()
return self._frame
end
function LabeledSliderClass:SetValue(newValue)
if self._sliderValue.Value ~= newValue then
self._sliderValue.Value = newValue
end
end
function LabeledSliderClass:GetValue()
return self._sliderValue.Value
end
return LabeledSliderClass

10
src/Lualibs/LabeledTextInput.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
declare class LabeledTextInput {
constructor(nameSuffix: string, labelText: string, defaultValue?: string);
SetValueChangedFunction(fn: (value: string) => void): void;
GetFrame(): Instance;
GetValue(): string;
GetMaxGraphemes(): number;
SetMaxGraphemes(value: number): void;
SetValue(value: string): void;
}
export = LabeledTextInput;

View File

@@ -0,0 +1,122 @@
----------------------------------------
--
-- LabeledTextInput.lua
--
-- Creates a frame containing a label and a text input control.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
local kTextInputWidth = 100
local kTextBoxInternalPadding = 4
LabeledTextInputClass = {}
LabeledTextInputClass.__index = LabeledTextInputClass
function LabeledTextInputClass.new(nameSuffix, labelText, defaultValue)
local self = {}
setmetatable(self, LabeledTextInputClass)
-- Note: we are using "graphemes" instead of characters.
-- In modern text-manipulation-fu, what with internationalization,
-- emojis, etc, it's not enough to count characters, particularly when
-- concerned with "how many <things> am I rendering?".
-- We are using the
self._MaxGraphemes = 10
self._valueChangedFunction = nil
local defaultValue = defaultValue or ""
local frame = GuiUtilities.MakeStandardFixedHeightFrame('TextInput ' .. nameSuffix)
self._frame = frame
local label = GuiUtilities.MakeStandardPropertyLabel(labelText)
label.Parent = frame
self._label = label
self._value = defaultValue
-- Dumb hack to add padding to text box,
local textBoxWrapperFrame = Instance.new("Frame")
textBoxWrapperFrame.Name = "Wrapper"
textBoxWrapperFrame.Size = UDim2.new(0, kTextInputWidth, 0.6, 0)
textBoxWrapperFrame.Position = UDim2.new(0, GuiUtilities.StandardLineElementLeftMargin, .5, 0)
textBoxWrapperFrame.AnchorPoint = Vector2.new(0, .5)
textBoxWrapperFrame.Parent = frame
GuiUtilities.syncGuiElementInputFieldColor(textBoxWrapperFrame)
GuiUtilities.syncGuiElementBorderColor(textBoxWrapperFrame)
local textBox = Instance.new("TextBox")
textBox.Parent = textBoxWrapperFrame
textBox.Name = "TextBox"
textBox.Text = defaultValue
textBox.Font = Enum.Font.SourceSans
textBox.TextSize = 15
textBox.BackgroundTransparency = 1
textBox.TextXAlignment = Enum.TextXAlignment.Left
textBox.Size = UDim2.new(1, -kTextBoxInternalPadding, 1, GuiUtilities.kTextVerticalFudge)
textBox.Position = UDim2.new(0, kTextBoxInternalPadding, 0, 0)
textBox.ClipsDescendants = true
GuiUtilities.syncGuiElementFontColor(textBox)
textBox:GetPropertyChangedSignal("Text"):connect(function()
-- Never let the text be too long.
-- Careful here: we want to measure number of graphemes, not characters,
-- in the text, and we want to clamp on graphemes as well.
if (utf8.len(self._textBox.Text) > self._MaxGraphemes) then
local count = 0
for start, stop in utf8.graphemes(self._textBox.Text) do
count = count + 1
if (count > self._MaxGraphemes) then
-- We have gone one too far.
-- clamp just before the beginning of this grapheme.
self._textBox.Text = string.sub(self._textBox.Text, 1, start-1)
break
end
end
-- Don't continue with rest of function: the resetting of "Text" field
-- above will trigger re-entry. We don't need to trigger value
-- changed function twice.
return
end
self._value = self._textBox.Text
if (self._valueChangedFunction) then
self._valueChangedFunction(self._value)
end
end)
self._textBox = textBox
return self
end
function LabeledTextInputClass:SetValueChangedFunction(vcf)
self._valueChangedFunction = vcf
end
function LabeledTextInputClass:GetFrame()
return self._frame
end
function LabeledTextInputClass:GetValue()
return self._value
end
function LabeledTextInputClass:GetMaxGraphemes()
return self._MaxGraphemes
end
function LabeledTextInputClass:SetMaxGraphemes(newValue)
self._MaxGraphemes = newValue
end
function LabeledTextInputClass:SetValue(newValue)
if self._value ~= newValue then
self._textBox.Text = newValue
end
end
return LabeledTextInputClass

4214
src/Lualibs/RbxGui.lua Normal file

File diff suppressed because it is too large Load Diff

7
src/Lualibs/StatefulImageButton.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare class StatefulImageButtonClass {
constructor(buttonName: string, imageAsset: string, buttonSize: UDim2);
setSelected(selected: boolean): void;
getSelected(): boolean;
getButton(): Instance;
}
export = StatefulImageButtonClass;

View File

@@ -0,0 +1,95 @@
----------------------------------------
--
-- StatefulImageButton.lua
--
-- Image button.
-- Has custom image for when "selected"
-- Uses shading to indicate hover and click states.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
StatefulImageButtonClass = {}
StatefulImageButtonClass.__index = StatefulImageButtonClass
function StatefulImageButtonClass.new(buttonName, imageAsset, buttonSize)
local self = {}
setmetatable(self, StatefulImageButtonClass)
local button = Instance.new("ImageButton")
button.Parent = parent
button.Image = imageAsset
button.BackgroundTransparency = 1
button.BorderSizePixel = 0
button.Size = buttonSize
button.Name = buttonName
self._button = button
self._hovered = false
self._clicked = false
self._selected = false
button.InputBegan:connect(function(input)
if (input.UserInputType == Enum.UserInputType.MouseMovement) then
self._hovered = true
self:_updateButtonVisual()
end
end)
button.InputEnded:connect(function(input)
if (input.UserInputType == Enum.UserInputType.MouseMovement) then
self._hovered = false
self._clicked = false
self:_updateButtonVisual()
end
end)
button.MouseButton1Down:connect(function()
self._clicked = true
self:_updateButtonVisual()
end)
button.MouseButton1Up:connect(function()
self._clicked = false
self:_updateButtonVisual()
end)
self:_updateButtonVisual()
return self
end
function StatefulImageButtonClass:_updateButtonVisual()
if (self._selected) then
self._button.ImageTransparency = 0
self._button.ImageColor3 = Color3.new(1,1,1)
else
self._button.ImageTransparency = 0.5
self._button.ImageColor3 = Color3.new(.5,.5,.5)
end
if (self._clicked) then
self._button.BackgroundTransparency = 0.8
elseif (self._hovered) then
self._button.BackgroundTransparency = 0.9
else
self._button.BackgroundTransparency = 1
end
end
function StatefulImageButtonClass:setSelected(selected)
self._selected = selected
self:_updateButtonVisual()
end
function StatefulImageButtonClass:getSelected()
return self._selected
end
function StatefulImageButtonClass:getButton()
return self._button
end
return StatefulImageButtonClass

View File

@@ -0,0 +1,6 @@
declare class VerticalScrollingFrame {
constructor(suffix: string);
GetContentsFrame(): Instance;
GetSectionFrame(): Instance;
}
export = VerticalScrollingFrame;

View File

@@ -0,0 +1,87 @@
----------------------------------------
--
-- VerticalScrollingFrame.lua
--
-- Creates a scrolling frame that automatically updates canvas size
--
----------------------------------------
local GuiUtilities = require(script.Parent.GuiUtilities)
local VerticalScrollingFrame = {}
VerticalScrollingFrame.__index = VerticalScrollingFrame
function VerticalScrollingFrame.new(suffix)
local self = {}
setmetatable(self, VerticalScrollingFrame)
local section = Instance.new("Frame")
section.BorderSizePixel = 0
section.Size = UDim2.new(1, 0, 1, 0)
section.Position = UDim2.new(0, 0, 0, 0)
section.BackgroundTransparency = 1
section.Name = "VerticalScrollFrame" .. suffix
local scrollBackground = Instance.new("Frame")
scrollBackground.Name = "ScrollbarBackground"
scrollBackground.BackgroundColor3 = Color3.fromRGB(238, 238, 238)
scrollBackground.BorderColor3 = Color3.fromRGB(182, 182, 182)
scrollBackground.Size = UDim2.new(0, 15, 1, -2)
scrollBackground.Position = UDim2.new(1, -16, 0, 1)
scrollBackground.Parent = section
scrollBackground.ZIndex = 2;
local scrollFrame = Instance.new("ScrollingFrame")
scrollFrame.Name = "ScrollFrame" .. suffix
scrollFrame.VerticalScrollBarPosition = Enum.VerticalScrollBarPosition.Right
scrollFrame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
scrollFrame.ElasticBehavior = Enum.ElasticBehavior.Never
scrollFrame.ScrollBarThickness = 17
scrollFrame.BorderSizePixel = 0
scrollFrame.BackgroundTransparency = 1
scrollFrame.ZIndex = 2
scrollFrame.TopImage = "http://www.roblox.com/asset/?id=1533255544"
scrollFrame.MidImage = "http://www.roblox.com/asset/?id=1535685612"
scrollFrame.BottomImage = "http://www.roblox.com/asset/?id=1533256504"
scrollFrame.Size = UDim2.new(1, 0, 1, 0)
scrollFrame.Position = UDim2.new(0, 0, 0, 0)
scrollFrame.Parent = section
local uiListLayout = Instance.new("UIListLayout")
uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder
uiListLayout.Parent = scrollFrame
self._section = section
self._scrollFrame = scrollFrame
self._scrollBackground = scrollBackground
self._uiListLayout = uiListLayout
scrollFrame:GetPropertyChangedSignal("AbsoluteSize"):connect(function() self:_updateScrollingFrameCanvas() end)
uiListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):connect(function() self:_updateScrollingFrameCanvas() end)
self:_updateScrollingFrameCanvas()
GuiUtilities.syncGuiElementScrollColor(scrollFrame)
GuiUtilities.syncGuiElementBorderColor(scrollBackground)
GuiUtilities.syncGuiElementTitleColor(scrollBackground)
return self
end
function VerticalScrollingFrame:_updateScrollbarBackingVisibility()
self._scrollBackground.Visible = self._scrollFrame.AbsoluteSize.y < self._uiListLayout.AbsoluteContentSize.y
end
function VerticalScrollingFrame:_updateScrollingFrameCanvas()
self._scrollFrame.CanvasSize = UDim2.new(0, 0, 0, self._uiListLayout.AbsoluteContentSize.Y)
self:_updateScrollbarBackingVisibility()
end
function VerticalScrollingFrame:GetContentsFrame()
return self._scrollFrame
end
function VerticalScrollingFrame:GetSectionFrame()
return self._section
end
return VerticalScrollingFrame

View File

@@ -0,0 +1,8 @@
declare class VerticallyScalingListFrame {
constructor(nameSuffix: string);
AddBottomPadding(): void;
GetFrame(): Instance;
AddChild(childFrame: Instance): void;
SetCallbackOnResize(callback: () => void): void;
}
export = VerticallyScalingListFrame;

View File

@@ -0,0 +1,73 @@
----------------------------------------
--
-- VerticallyScalingListFrame
--
-- Creates a frame that organizes children into a list layout.
-- Will scale dynamically as children grow.
--
----------------------------------------
GuiUtilities = require(script.Parent.GuiUtilities)
VerticallyScalingListFrame = {}
VerticallyScalingListFrame.__index = VerticallyScalingListFrame
local kBottomPadding = 10
function VerticallyScalingListFrame.new(nameSuffix)
local self = {}
setmetatable(self, VerticallyScalingListFrame)
self._resizeCallback = nil
local frame = Instance.new('Frame')
frame.Name = 'VSLFrame' .. nameSuffix
frame.Size = UDim2.new(1, 0, 0, height)
frame.BackgroundTransparency = 0
frame.BorderSizePixel = 0
GuiUtilities.syncGuiElementBackgroundColor(frame)
self._frame = frame
local uiListLayout = Instance.new('UIListLayout')
uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder
uiListLayout.Parent = frame
self._uiListLayout = uiListLayout
local function updateSizes()
self._frame.Size = UDim2.new(1, 0, 0, uiListLayout.AbsoluteContentSize.Y)
if (self._resizeCallback) then
self._resizeCallback()
end
end
self._uiListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):connect(updateSizes)
updateSizes()
self._childCount = 0
return self
end
function VerticallyScalingListFrame:AddBottomPadding()
local frame = Instance.new("Frame")
frame.Name = "BottomPadding"
frame.BackgroundTransparency = 1
frame.Size = UDim2.new(1, 0, 0, kBottomPadding)
frame.LayoutOrder = 1000
frame.Parent = self._frame
end
function VerticallyScalingListFrame:GetFrame()
return self._frame
end
function VerticallyScalingListFrame:AddChild(childFrame)
childFrame.LayoutOrder = self._childCount
self._childCount = self._childCount + 1
childFrame.Parent = self._frame
end
function VerticallyScalingListFrame:SetCallbackOnResize(callback)
self._resizeCallback = callback
end
return VerticallyScalingListFrame

View File

@@ -0,0 +1,9 @@
export class WidgetButton {
constructor(toolbar: PluginToolbar, widget: DockWidgetPluginGui, buttonText: string, icon: string) {
const widgetButton = toolbar.CreateButton(buttonText, buttonText, icon);
widgetButton.Click.Connect(() => {
widgetButton.SetActive(!widget.Enabled);
widget.Enabled = !widget.Enabled;
});
}
}

30
src/Utils/Gui.ts Normal file
View File

@@ -0,0 +1,30 @@
export function createAnchor(model: Folder, position: Vector3) {
// Create Anchor for Adornments
const anchor = new Instance("Part");
anchor.Anchored = true;
anchor.CanCollide = false;
anchor.Transparency = 1;
anchor.Size = new Vector3(0.1, 0.1, 0.1);
anchor.Position = position;
anchor.Parent = model;
anchor.Name = "_GizmoAnchor";
return anchor;
}
export function syncGuiColors(...objects: GuiObject[]) {
function setColors() {
for (const guiObject of objects) {
const studio = settings().Studio as Studio & { Theme: StudioTheme };
pcall(() => {
guiObject.BackgroundColor3 = studio.Theme.GetColor(Enum.StudioStyleGuideColor.MainBackground);
});
pcall(() => {
if (guiObject.IsA("TextLabel")) {
guiObject.TextColor3 = studio.Theme.GetColor(Enum.StudioStyleGuideColor.MainText);
}
});
}
}
setColors();
settings().Studio.ThemeChanged.Connect(setColors);
}

18
src/Utils/Room.ts Normal file
View File

@@ -0,0 +1,18 @@
type RoomConfig = Configuration & {
RoomId: IntValue;
RoomType: StringValue;
Origin: Vector3Value;
End: Vector3Value;
[key: `Exit_${number}`]: CFrameValue;
};
export function checkRoomConfig(obj: Instance): obj is RoomConfig {
return (
(obj.IsA("Configuration") &&
obj.FindFirstChild("RoomId")?.IsA("IntValue") &&
obj.FindFirstChild("RoomType")?.IsA("StringValue") &&
obj.FindFirstChild("Origin")?.IsA("Vector3Value") &&
obj.FindFirstChild("End")?.IsA("Vector3Value")) ??
false
);
}

209
src/Widget/RoomWidget.lua Normal file
View File

@@ -0,0 +1,209 @@
local RoomWidget = {}
local ScrollingFrame = require(script.Parent.Parent.Parent.StudioWidgets.VerticalScrollingFrame)
local VerticallyScalingListFrame = require(script.Parent.Parent.Parent.StudioWidgets.VerticallyScalingListFrame)
local CollapsibleTitledSection = require(script.Parent.Parent.Parent.StudioWidgets.CollapsibleTitledSection)
local LabeledTextInput = require(script.Parent.Parent.Parent.StudioWidgets.LabeledTextInput)
local CustomTextButton = require(script.Parent.Parent.Parent.StudioWidgets.CustomTextButton)
local guiModule = require(script.Parent.Parent.Parent.Tools.Gui)
local UseLessModule = require(script.Parent.Parent.Parent.Tools.Useless)
function RoomWidget:new(plugin:Plugin,adornmentContainer:Folder,pluginModel:Folder)
self = setmetatable({}, {__index = RoomWidget})
self.adornmentContainer = adornmentContainer
self.pluginModel = pluginModel
-- Create Widget
self.info = DockWidgetPluginGuiInfo.new(
Enum.InitialDockState.Left,
false,
false,
200,
300,
150,
150
)
self.widget = plugin:CreateDockWidgetPluginGui(
"RoomWidget",
self.info
)
self.widget.Title = "Room Info"
-- Create Widget Components
-- Create No Room Label
self.noRoomLabel = Instance.new("TextLabel")
self.noRoomLabel.Name = "NoSelect"
self.noRoomLabel.Text = "Select a room to use this widget."
self.noRoomLabel.Size = UDim2.new(1,0,1,0)
guiModule.syncGuiColors({ self.noRoomLabel })
-- Create Scrolling Frame
self.scrollFrame = ScrollingFrame.new("RoomScroll")
-- Create Vertical Scaling List Frame
self.listFrame = VerticallyScalingListFrame.new("RoomWidget")
-- Create Room Collapse
self.roomCollapse = CollapsibleTitledSection.new(
"suffix", -- name suffix of the gui object
"Room", -- the text displayed beside the collapsible arrow
true, -- have the content frame auto-update its size?
true, -- minimizable?
false -- minimized by default?
)
-- Create Exit Collapse
self.exitCollapse = CollapsibleTitledSection.new(
"ExitCollapse", -- name suffix of the gui object
"Exit", -- title text of the collapsible arrow
true, -- have the content frame auto-update its size?
true, -- minimizable?
false -- minimized by default?
)
-- Create TextInput
self.roomIdValue = Instance.new("IntValue")
self.roomIdLabel = LabeledTextInput.new(
"RoomId", -- name suffix of gui object
"Room Id", -- title text of the multi choice
"0" -- default value
)
self.roomIdLabel:SetValueChangedFunction(
function (value)
self.roomIdValue.Value = value
end
)
self.typeValue = Instance.new("StringValue")
self.typeLabel = LabeledTextInput.new(
"RoomType", -- name suffix of gui object
"Room Type", -- title text of the multi choice
"" -- default value
)
self.typeLabel:SetMaxGraphemes(255)
self.typeLabel:SetValueChangedFunction(
function (value)
self.typeValue.Value = value
end
)
-- Setup Widget
self.roomIdLabel:GetFrame().Parent = self.roomCollapse:GetContentsFrame()
self.typeLabel:GetFrame().Parent = self.roomCollapse:GetContentsFrame()
self.listFrame:AddChild(self.roomCollapse:GetSectionFrame())
self.listFrame:AddChild(self.exitCollapse:GetSectionFrame())
self.listFrame:AddBottomPadding()
self.listFrame:GetFrame().Parent = self.scrollFrame:GetContentsFrame()
self.scrollFrame:GetSectionFrame().Parent = self.widget
self.noRoomLabel.Parent = self.widget
return self
end
function RoomWidget:UpdateValue(roomIdValue:IntValue,roomTypeValue:StringValue,roomExit:{CFrameValue})
self.roomIdValue = roomIdValue
self.roomIdLabel:SetValue(roomIdValue.Value)
self.typeValue = roomTypeValue
self.typeLabel:SetValue(roomTypeValue.Value)
end
function RoomWidget:SetActive(active:boolean)
self.noRoomLabel.Visible = not active
self.scrollFrame.Visible = active
end
function RoomWidget:_clearAdornment()
self.adornmentContainer:ClearAllChildren()
self.pluginModel:ClearAllChildren()
end
function RoomWidget:ReloadExit(exits:{CFrameValue})
self.exitCollapse:GetContentsFrame():ClearAllChildren()
for _,exit in exits do
local exitCollapse = CollapsibleTitledSection.new(
"ExitCollapse_"..exit.Name, -- name suffix of the gui object
exit.Name, -- the text displayed beside the collapsible arrow
true, -- have the content frame auto-update its size?
true, -- minimizable?
false -- minimized by default?
)
local button = CustomTextButton.new(
"edit_button", -- name of the gui object
"Edit" -- the text displayed on the button
)
button:GetButton().Activated:Connect(
function(inputObject: InputObject, clickCount: number)
end
)
-- Handle widget
exitCollapse:GetContentsFrame().Parent = self.exitCollapse:GetSectionFrame()
end
end
function RoomWidget:LoadExitMoveHandles(exit: CFrameValue)
self:_clearAdornment()
local function createAnchor(position: Vector3)
-- Create Anchor for Adornments
local anchor = Instance.new("Part")
anchor.Anchored = true
anchor.CanCollide = false
anchor.Transparency = 1
anchor.Size = Vector3.new(0.1, 0.1, 0.1)
anchor.Position = position
anchor.Parent = self.pluginModel
anchor.Name = "_GizmoAnchor"
return anchor
end
local anchor = createAnchor(exit.Value.Position)
local handles = Instance.new("Handles")
handles.Adornee = anchor
handles.Parent = self.adornmentContainer
local currentVector = Vector3.new(0, 0, 0)
handles.MouseButton1Down:Connect(
function(face: Enum.NormalId)
currentVector = anchor.Position
end
)
handles.MouseDrag:Connect(
function(face: Enum.NormalId, distance: number)
-- Apply the changes to the anchor's position
if face == Enum.NormalId.Top or face == Enum.NormalId.Right or face == Enum.NormalId.Back then
anchor.Position = UseLessModule.roundVector3(currentVector + Vector3.new(
UseLessModule.bton(face == Enum.NormalId.Right) * distance,
UseLessModule.bton(face == Enum.NormalId.Top) * distance,
UseLessModule.bton(face == Enum.NormalId.Back) * distance
))
else
anchor.Position = UseLessModule.roundVector3(currentVector - Vector3.new(
UseLessModule.bton(face == Enum.NormalId.Left) * distance,
UseLessModule.bton(face == Enum.NormalId.Bottom) * distance,
UseLessModule.bton(face == Enum.NormalId.Front) * distance
))
end
end
)
handles.MouseButton1Up:Connect(
function(face: Enum.NormalId)
local newpos = anchor.Position
position.Value = newpos
end
)
end
return RoomWidget

83
src/Widget/RoomWidget.ts Normal file
View File

@@ -0,0 +1,83 @@
import CollapsibleTitledSection from "Lualibs/CollapsibleTitledSection";
import LabeledTextInput from "Lualibs/LabeledTextInput";
import VerticallyScalingListFrame from "Lualibs/VerticallyScalingListFrame";
import VerticalScrollingFrame from "Lualibs/VerticalScrollingFrame";
import { syncGuiColors } from "Utils/Gui";
export class RoomWidget {
plugin: Plugin;
model: Folder;
info: DockWidgetPluginGuiInfo;
widget: DockWidgetPluginGui;
noRoomLabel: TextLabel;
scrollFrame = new VerticalScrollingFrame("RoomScroll");
listFrame = new VerticallyScalingListFrame("RoomWidget");
roomCollapse = new CollapsibleTitledSection(
"roomCollapse", // name suffix of the gui object
"Room", // the text displayed beside the collapsible arrow
true, // have the content frame auto-update its size?
true, // minimizable?
false, // minimized by default?
);
exitCollapse = new CollapsibleTitledSection(
"ExitCollapse", // name suffix of the gui object
"Exit", // title text of the collapsible arrow
true, // have the content frame auto-update its size?
true, // minimizable?
false, // minimized by default?
);
roomIdValue = new Instance("IntValue");
roomIdInput = new LabeledTextInput(
"RoomId", // name suffix of gui object
"Room Id", // title text of the multi choice
"0", // default value
);
roomTypeValue = new Instance("StringValue");
roomTypeInput = new LabeledTextInput(
"RoomType", // name suffix of gui object
"Room Type", // title text of the multi choice
"", // default value
);
constructor(plugin: Plugin, model: Folder) {
this.plugin = plugin;
this.model = model;
this.info = new DockWidgetPluginGuiInfo(Enum.InitialDockState.Left, false, false, 200, 300, 150, 150);
this.widget = plugin.CreateDockWidgetPluginGui("RoomWidget", this.info);
(this.widget as unknown as { Title: string }).Title = "Room Info";
this.noRoomLabel = new Instance("TextLabel");
this.noRoomLabel.Text = "Select a room to use this widget.";
this.noRoomLabel.Size = new UDim2(1, 0, 1, 0);
syncGuiColors(this.noRoomLabel);
this.roomIdInput.SetValueChangedFunction((value) => {
this.roomIdValue.Value = tonumber(value) ?? 0;
});
this.roomTypeInput.SetMaxGraphemes(255);
this.roomTypeInput.SetValueChangedFunction((value) => {
this.roomTypeValue.Value = value;
});
// Setup Widget
this.roomIdInput.GetFrame().Parent = this.roomCollapse.GetContentsFrame();
this.roomTypeInput.GetFrame().Parent = this.roomCollapse.GetContentsFrame();
this.listFrame.AddChild(this.roomCollapse.GetSectionFrame());
this.listFrame.AddChild(this.exitCollapse.GetSectionFrame());
this.listFrame.AddBottomPadding();
this.listFrame.GetFrame().Parent = this.scrollFrame.GetContentsFrame();
this.scrollFrame.GetSectionFrame().Parent = this.widget;
this.noRoomLabel.Parent = this.widget;
}
UpdateValue() {
}
SetActive(active: boolean) {
this.noRoomLabel.Visible = !active;
}
}

55
src/index.server.ts Normal file
View File

@@ -0,0 +1,55 @@
import Signal from "@rbxts/signal";
import { HandlesArea } from "Adornment/Area";
import { WidgetButton } from "Toolbar/WidgetButton";
import { checkRoomConfig } from "Utils/Room";
// Services
const Selection = game.GetService("Selection");
const CoreGui = game.GetService("CoreGui");
// Module
// const UseLessModule = require(script.Parent.Tools.Useless)
// const RoomWidget = require(script.Parent.Widget.Room.Main)
// const WidgetButton = require(script.Parent.Widget.WidgetTogger)
// const ToolsButtons = require(script.Parent.Buttons.ToolsButtons)
// Create Widget
// const roomWidget = RoomWidget.new(plugin)
// Create Toolbar
const toolbar = plugin.CreateToolbar("Next Station Plugin");
// Create Buttons
// const roomWidgetButton = new WidgetButton(toolbar, roomWidget.widget, "Room Info", "rbxassetid.//14978048121");
// const createRoomButton = ToolsButtons.createNewRoomButton(toolbar)
// Create CoreGui Folders
const pluginModel = new Instance("Folder");
pluginModel.Name = "_constGizmoContainer";
pluginModel.Parent = CoreGui;
plugin.Unloading.Connect(() => {
pluginModel.Destroy();
});
// Selection Room Config Controller
function clearAdornment() {
pluginModel.ClearAllChildren();
}
(Selection as unknown as { SelectionChanged: Signal }).SelectionChanged.Connect(() => {
// Get Selection
const selectedObjects = Selection.Get();
// Reset Adornments
clearAdornment();
// roomWidget.SetActive(false)
if (!selectedObjects.isEmpty()) {
const selected = selectedObjects[0];
const config = selected.FindFirstChild("RoomConfig");
if (selected.IsA("Model") && config && checkRoomConfig(config)) {
const roomArea = new HandlesArea(pluginModel, config.Origin.Value, config.End.Value);
roomArea.originValueChanged.Connect((value) => (config.Origin.Value = value));
roomArea.tipValueChanged.Connect((value) => (config.End.Value = value));
}
}
});

28
tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
// required
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
"module": "commonjs",
"moduleResolution": "Node",
"noLib": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"moduleDetection": "force",
"strict": true,
"target": "ESNext",
"typeRoots": ["node_modules/@rbxts"],
// configurable
"types": ["types/plugin"],
"rootDir": "src",
"outDir": "out",
"baseUrl": "src",
"incremental": true,
"tsBuildInfoFile": "out/tsconfig.tsbuildinfo"
}
}