This commit is contained in:
2025-09-19 23:56:19 +02:00
parent f838fe45e7
commit 2a878f41b5
12 changed files with 227 additions and 256 deletions

View File

@@ -1,27 +0,0 @@
{
"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"
}
}

51
eslint.config.mjs Normal file
View File

@@ -0,0 +1,51 @@
import { defineConfig, globalIgnores } from "eslint/config";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import robloxTs from "eslint-plugin-roblox-ts";
import prettier from "eslint-plugin-prettier";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default defineConfig([
globalIgnores(["out"]),
{
extends: compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:roblox-ts/recommended-legacy",
"plugin:prettier/recommended",
),
plugins: {
"@typescript-eslint": typescriptEslint,
robloxTs,
prettier,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 2018,
sourceType: "module",
parserOptions: {
jsx: true,
useJSXTextNode: true,
project: "./tsconfig.json",
},
},
rules: {
"prettier/prettier": "warn",
},
},
]);

2
package-lock.json generated
View File

@@ -12,6 +12,8 @@
"@rbxts/signal": "^1.1.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.35.0",
"@rbxts/compiler-types": "^3.0.0-types.0",
"@rbxts/types": "^1.0.881",
"@typescript-eslint/eslint-plugin": "^8.43.0",

View File

@@ -6,13 +6,17 @@
"scripts": {
"build": "rbxtsc",
"watch": "rbxtsc -w",
"build-to-roblox": "rbxtsc && rojo build --plugin \"next-station.rbxm\""
"build-to-roblox": "rbxtsc && rojo build --plugin \"next-station.rbxm\"",
"lint": "eslint",
"lint-fix": "eslint --fix"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.35.0",
"@rbxts/compiler-types": "^3.0.0-types.0",
"@rbxts/types": "^1.0.881",
"@typescript-eslint/eslint-plugin": "^8.43.0",

View File

@@ -0,0 +1,46 @@
import Signal from "@rbxts/signal";
import { createAnchor } from "Utils/Gui";
import { snapCFrameRotation } from "Utils/Math";
export class ArcHandles {
currentCframe: CFrame = new CFrame();
anchor: Part;
valueChanged = new Signal<(newValue: CFrame) => void>();
constructor(
model: Folder,
startPosition: CFrame = CFrame.identity,
anchor = createAnchor(model, startPosition.Position),
) {
this.anchor = anchor;
anchor.CFrame = startPosition;
const handles = new Instance("ArcHandles");
handles.Adornee = this.anchor;
handles.Parent = model;
handles.MouseButton1Down.Connect(() => {
this.currentCframe = this.anchor.CFrame;
});
handles.MouseDrag.Connect((axis, distance) => {
// Apply the rotation to the anchor's Cframe
let rotation = CFrame.identity;
switch (axis) {
case Enum.Axis.X:
rotation = CFrame.Angles(distance, 0, 0);
break;
case Enum.Axis.Y:
rotation = CFrame.Angles(0, distance, 0);
break;
case Enum.Axis.Z:
rotation = CFrame.Angles(0, 0, distance);
break;
}
this.anchor.CFrame = this.currentCframe.mul(snapCFrameRotation(rotation));
});
handles.MouseButton1Up.Connect(() => {
const newCFrame = this.anchor.CFrame;
this.valueChanged.Fire(newCFrame);
});
}
}

12
src/Adornment/Exit.ts Normal file
View File

@@ -0,0 +1,12 @@
export function doorAdornment(model: Folder, anchor: Part) {
const doorAdornment = new Instance("BoxHandleAdornment");
doorAdornment.Adornee = anchor;
doorAdornment.Size = new Vector3(5, 7, 1);
doorAdornment.AlwaysOnTop = true;
doorAdornment.ZIndex = 5;
doorAdornment.Color3 = new Color3(1, 0, 0);
doorAdornment.Transparency = 0.8;
doorAdornment.Parent = model;
doorAdornment.CFrame = new CFrame(0, 3.5, 0);
return doorAdornment;
}

View File

@@ -6,6 +6,7 @@ export class Handles {
currentVector: Vector3 = new Vector3();
anchor: Part;
valueChanged = new Signal<(newValue: Vector3) => void>();
handles: HandlesBase;
constructor(model: Folder, startPosition: Vector3 = Vector3.zero, anchor = createAnchor(model, startPosition)) {
this.anchor = anchor;
@@ -48,5 +49,7 @@ export class Handles {
const newpos = this.anchor.Position;
this.valueChanged.Fire(newpos);
});
this.handles = handles;
}
}

View File

@@ -1,4 +1,4 @@
export function createAnchor(model: Folder, position: Vector3) {
export function createAnchor(model: Folder, position: Vector3 = Vector3.zero) {
// Create Anchor for Adornments
const anchor = new Instance("Part");
anchor.Anchored = true;

View File

@@ -1,3 +1,14 @@
export function roundVector(vector: Vector3) {
return new Vector3(math.round(vector.X * 4) / 4, math.round(vector.Y * 4) / 4, math.round(vector.Z * 4) / 4);
}
export function snapCFrameRotation(cf: CFrame) {
let [x, y, z] = cf.ToEulerAnglesXYZ();
x = math.round(math.deg(x) / 10) * 10;
y = math.round(math.deg(y) / 10) * 10;
z = math.round(math.deg(z) / 10) * 10;
const snappedRot = CFrame.Angles(math.rad(x), math.rad(y), math.rad(z));
return new CFrame(cf.Position).mul(snappedRot);
}

View File

@@ -1,3 +1,6 @@
import { ArcHandles } from "Adornment/ArcHandles";
import { doorAdornment } from "Adornment/Exit";
import { Handles } from "Adornment/Handles";
import CollapsibleTitledSection from "Lualibs/CollapsibleTitledSection";
import CustomTextButton from "Lualibs/CustomTextButton";
import LabeledTextInput from "Lualibs/LabeledTextInput";
@@ -22,7 +25,7 @@ export class RoomWidget {
true, // minimizable?
false, // minimized by default?
);
exitCollapse = new CollapsibleTitledSection(
exitsCollapse = 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?
@@ -72,7 +75,7 @@ export class RoomWidget {
const typeTextBox = this.roomTypeInput.GetFrame().FindFirstChildWhichIsA("TextBox", true)!;
typeTextBox.ClearTextOnFocus = false;
this.listFrame.AddChild(this.roomCollapse.GetSectionFrame());
this.listFrame.AddChild(this.exitCollapse.GetSectionFrame());
this.listFrame.AddChild(this.exitsCollapse.GetSectionFrame());
this.listFrame.AddBottomPadding();
@@ -85,32 +88,101 @@ export class RoomWidget {
this.roomIdInput.SetValue(tostring(config.RoomId.Value));
this.roomTypeValue = config.RoomType;
this.roomTypeInput.SetValue(config.RoomType.Value);
const exits = config
.GetChildren()
.filter(
(value) => !string.match(value.Name, "^Exit_[0-9]+$").isEmpty() && value.IsA("CFrameValue"),
) as CFrameValue[];
this.ReloadExits(exits, config);
}
SetActive(active: boolean) {
this.noRoomLabel.Visible = !active;
}
ReloadExits(exits: CFrameValue[]) {
this.exitCollapse.GetContentsFrame().ClearAllChildren();
ReloadExits(exits: CFrameValue[], config: RoomConfig) {
this.exitsCollapse.GetContentsFrame().ClearAllChildren();
const exitListFrame = new VerticallyScalingListFrame("RoomWidget");
for (const exit of exits) {
const exitCollapse = new CollapsibleTitledSection(
"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?
);
const button = new CustomTextButton(
const posbutton = new CustomTextButton(
"edit_button", // name of the gui object
"Edit", // the text displayed on the button
`Edit ${exit.Name} position`, // the text displayed on the button
);
(button.GetButton() as ImageButton).Activated.Connect(() => {
const posbuttonobject = posbutton.GetButton() as ImageButton;
posbuttonobject.Activated.Connect(() => {
this.LoadExitMoveHandles(exit);
});
posbuttonobject.Size = new UDim2(0, 150, 0, 20);
// Handle widget
exitCollapse.GetContentsFrame().Parent = this.exitCollapse.GetSectionFrame();
const rotbutton = new CustomTextButton(
"edit_button", // name of the gui object
`Edit ${exit.Name} rotation`, // the text displayed on the button
);
const rotbuttonobject = rotbutton.GetButton() as ImageButton;
rotbuttonobject.Activated.Connect(() => {
this.LoadExitRotationHandles(exit);
});
rotbuttonobject.Size = new UDim2(0, 150, 0, 20);
exitListFrame.AddChild(posbuttonobject);
exitListFrame.AddChild(rotbuttonobject);
}
const rotbutton = new CustomTextButton(
"create_button", // name of the gui object
`Create a Exit`, // the text displayed on the button
);
const createbutobject = rotbutton.GetButton() as ImageButton;
createbutobject.Activated.Connect(() => {
const exit = new Instance("CFrameValue");
exit.Parent = config;
exit.Value = new CFrame(config.Origin.Value);
exit.Name = `Exit_${exits.size()}`;
exits.push(exit);
this.ReloadExits(exits, config);
});
createbutobject.Size = new UDim2(0, 150, 0, 20);
exitListFrame.AddChild(createbutobject);
exitListFrame.AddBottomPadding();
exitListFrame.GetFrame().Parent = this.exitsCollapse.GetContentsFrame();
}
LoadExitMoveHandles(exit: CFrameValue) {
this.clearExitHandles();
const folder = new Instance("Folder");
folder.Parent = this.model;
folder.Name = "_exit_handle";
const exithandle = new Handles(folder, exit.Value.Position);
exithandle.valueChanged.Connect((value) => {
const oldvalue = exit.Value;
exit.Value = new CFrame(value).mul(oldvalue.sub(oldvalue.Position));
});
doorAdornment(folder, exithandle.anchor);
}
LoadExitRotationHandles(exit: CFrameValue) {
this.clearExitHandles();
const folder = new Instance("Folder");
folder.Parent = this.model;
folder.Name = "_exit_handle";
const exithandle = new ArcHandles(folder, exit.Value);
exithandle.valueChanged.Connect((value) => {
const oldvalue = exit.Value;
exit.Value = new CFrame(oldvalue.Position).mul(value.Rotation);
});
doorAdornment(folder, exithandle.anchor);
}
clearExitHandles() {
const childs = this.model.GetChildren();
childs
.filter((value) => value.Name === "_exit_handle")
.forEach((value) => {
value.Destroy();
});
}
}

View File

@@ -1,209 +0,0 @@
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

View File

@@ -27,6 +27,12 @@ const toolbar = plugin.CreateToolbar("Next Station Plugin");
// Create Buttons
new WidgetButton(toolbar, roomWidget.widget, "Room Info", "rbxassetid://14978048121");
createNewRoomButton(toolbar);
// const button = toolbar.CreateButton("debugUi", "Debug", "");
// button.Click.Connect(() => {
// const gui = new Instance("ScreenGui");
// roomWidget.widget.GetChildren().forEach((v) => (v.Parent = gui));
// gui.Parent = game.GetService("StarterGui");
// });
// Selection Room Config Controller
function clearAdornment() {