From 2a878f41b542365da11bf05b1b27b920df96c478 Mon Sep 17 00:00:00 2001 From: Azur Date: Fri, 19 Sep 2025 23:56:19 +0200 Subject: [PATCH] v1.0 --- .eslintrc | 27 ---- eslint.config.mjs | 51 ++++++++ package-lock.json | 2 + package.json | 6 +- src/Adornment/ArcHandles.ts | 46 +++++++ src/Adornment/Exit.ts | 12 ++ src/Adornment/Handles.ts | 3 + src/Utils/Gui.ts | 2 +- src/Utils/Math.ts | 11 ++ src/Widget/RoomWidget.ts | 108 +++++++++++++--- src/Widget/RoomWidget_legacy.lua | 209 ------------------------------- src/index.server.ts | 6 + 12 files changed, 227 insertions(+), 256 deletions(-) delete mode 100644 .eslintrc create mode 100644 eslint.config.mjs create mode 100644 src/Adornment/ArcHandles.ts create mode 100644 src/Adornment/Exit.ts delete mode 100644 src/Widget/RoomWidget_legacy.lua diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a9bc76a..0000000 --- a/.eslintrc +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c528290 --- /dev/null +++ b/eslint.config.mjs @@ -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", + }, + }, +]); diff --git a/package-lock.json b/package-lock.json index 1f91b78..cf3e1ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8e081f7..fa32c4a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Adornment/ArcHandles.ts b/src/Adornment/ArcHandles.ts new file mode 100644 index 0000000..11f6ade --- /dev/null +++ b/src/Adornment/ArcHandles.ts @@ -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); + }); + } +} diff --git a/src/Adornment/Exit.ts b/src/Adornment/Exit.ts new file mode 100644 index 0000000..114bb37 --- /dev/null +++ b/src/Adornment/Exit.ts @@ -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; +} diff --git a/src/Adornment/Handles.ts b/src/Adornment/Handles.ts index c1d8ff8..04abfbf 100644 --- a/src/Adornment/Handles.ts +++ b/src/Adornment/Handles.ts @@ -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; } } diff --git a/src/Utils/Gui.ts b/src/Utils/Gui.ts index 3716d59..6383d49 100644 --- a/src/Utils/Gui.ts +++ b/src/Utils/Gui.ts @@ -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; diff --git a/src/Utils/Math.ts b/src/Utils/Math.ts index 25b5cc6..67304cb 100644 --- a/src/Utils/Math.ts +++ b/src/Utils/Math.ts @@ -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); +} diff --git a/src/Widget/RoomWidget.ts b/src/Widget/RoomWidget.ts index 89ff753..d21b594 100644 --- a/src/Widget/RoomWidget.ts +++ b/src/Widget/RoomWidget.ts @@ -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(); + }); } } diff --git a/src/Widget/RoomWidget_legacy.lua b/src/Widget/RoomWidget_legacy.lua deleted file mode 100644 index a2a6fb6..0000000 --- a/src/Widget/RoomWidget_legacy.lua +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/index.server.ts b/src/index.server.ts index 8dad63b..3c17311 100644 --- a/src/index.server.ts +++ b/src/index.server.ts @@ -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() {