CFrame Assistant
A free, easy way to edit and get CFrame/Vector data.
(Despite the name, this plugin mainly uses vectors)
Features
- Easy selection of parts/vectors
- Instantly auto-updating data (no need to reselect parts constantly)
- Compatible with parts AND models
- Quick part/model editing
- Quick vector calculator
Preview
Code
local Selection = game:GetService("Selection")
local TextService = game:GetService("TextService")
local toolbar: PluginToolbar = plugin:CreateToolbar("CFrame Assistant")
local pluginButton = toolbar:CreateButton(
"CFrame Assistant",
"Get distance or rotation between CFrames",
"rbxassetid://81071142883942",
"CFrame Assistant"
)
local dockWidgetInfo = DockWidgetPluginGuiInfo.new(
Enum.InitialDockState.Left,
false,
false
)
local widget = plugin:CreateDockWidgetPluginGui(
"CFrame Assistant",
dockWidgetInfo
)
widget.Title = "CFrame Assistant"
local GUI = script.Parent:WaitForChild("GUI")
GUI.Parent = widget
local numOfSelectedParts = 0
local selectedPart1: BasePart
local selectedPart2: BasePart
local vectorNum1: Vector3
local vectorNum2: Vector3
-- GUI objects --
local chooseSection = GUI:FindFirstChild("Section Choose")
local infoSection = GUI:FindFirstChild("Section Info")
local editSection = GUI:FindFirstChild("Section Edit")
local arithmeticSection = GUI:FindFirstChild("Section Arithmetic")
local part1Text = chooseSection:FindFirstChild("Part1")
local part2Text = chooseSection:FindFirstChild("Part2")
local switchParts = chooseSection:FindFirstChild("Switch parts")
local positionXYZText = infoSection:FindFirstChild("PositionXYZData")
local distanceMagnitudeText = infoSection:FindFirstChild("DistanceMagnitudeData")
local rotationXYZText = infoSection:FindFirstChild("RotationXYZData")
local pivotToPivot = editSection:FindFirstChild("PivotToPivot")
local CFrameToPivot = editSection:FindFirstChild("CFrameToPivot")
local vectorNum1Text = arithmeticSection:FindFirstChild("VectorNum1")
local vectorNum2Text = arithmeticSection:FindFirstChild("VectorNum2")
local addVectors = arithmeticSection:FindFirstChild("AddData")
local subtractVectors = arithmeticSection:FindFirstChild("SubtractData")
local multiplyVectors = arithmeticSection:FindFirstChild("MultiplyData")
local divideVectors = arithmeticSection:FindFirstChild("DivideData")
local switchVectors = arithmeticSection:FindFirstChild("Switch vectors")
local errorText = arithmeticSection:FindFirstChild("ErrorMessage")
local function CheckTextFits(text: string, textObject: TextLabel)
if TextService:GetTextSize(text, textObject.TextSize, textObject.Font, textObject.AbsoluteSize).X > textObject.AbsoluteSize.X then
textObject.TextScaled = true
else
textObject.TextScaled = false
end
end
-- Update data
local function RoundVector(vector3:Vector3, decimalPlaces:number, convertToDeg:boolean?, returnVector:boolean?)
local round = 10 ^ decimalPlaces
local axes = {"X", "Y", "Z"}
local axesValues = {}
for _, axis in axes do
-- Complicated...
if convertToDeg then
axesValues[axis] = math.round(math.deg(vector3[axis]) * round) / round
else
axesValues[axis] = math.round(vector3[axis] * round) / round
end
end
local newVectorParams = {axesValues["X"], axesValues["Y"], axesValues["Z"]}
if returnVector then
-- Optional to prevent rounding errors
return Vector3.new(newVectorParams[1], newVectorParams[2], newVectorParams[3])
else
return newVectorParams
end
end
local function ConvertTableToString(tableToConvert: {number})
local newString = ""
for index = 1, #tableToConvert do
newString ..= tableToConvert[index]
if index ~= #tableToConvert then
newString ..= ", "
end
end
return newString
end
local function UpdateInfo()
if selectedPart1 and selectedPart2 then
-- Create info --
-- If part is a base part set CFrame to CFrame or else set to pivot
local selectedPart1CFrame = selectedPart1:IsA("BasePart") and selectedPart1.CFrame or selectedPart1:GetPivot()
local selectedPart2CFrame = selectedPart2:IsA("BasePart") and selectedPart2.CFrame or selectedPart2:GetPivot()
local selectedPart1Rotation = Vector3.new(selectedPart1CFrame:ToOrientation())
local selectedPart2Rotation = Vector3.new(selectedPart2CFrame:ToOrientation())
local positionDifferenceXYZ:Vector3 = selectedPart1CFrame.Position - selectedPart2CFrame.Position
positionXYZText.Text = ConvertTableToString(RoundVector(positionDifferenceXYZ, 3, false))
CheckTextFits(positionXYZText.Text, positionXYZText)
local distanceMagnitude = positionDifferenceXYZ.Magnitude
distanceMagnitudeText.Text = tostring(math.round(distanceMagnitude * 1000) / 1000)
CheckTextFits(distanceMagnitudeText.Text, distanceMagnitudeText)
local rotationDifferenceXYZ:Vector3 = selectedPart1Rotation - selectedPart2Rotation
rotationXYZText.Text = ConvertTableToString(RoundVector(rotationDifferenceXYZ, 3, true))
CheckTextFits(rotationXYZText.Text, rotationXYZText)
end
end
-- Update selected parts
local function UpdateText(selectedObject: BasePart, textObject: TextLabel)
local text
if selectedObject and (selectedObject:IsA("Model") or selectedObject:IsA("BasePart")) then
text = '"'..selectedObject.Name..'"'
else
text = "Invalid Object"
end
-- Make sure text fits
CheckTextFits(text, textObject)
textObject.Text = text
end
local CFrameChangeConnection1
local CFrameChangeConnection2
local function SetSelection(part1: BasePart, part2: BasePart)
UpdateText(part1, part1Text)
if part1 and (part1:IsA("Model") or part1:IsA("BasePart")) then
selectedPart1 = part1
else
selectedPart1 = nil
end
UpdateText(part2, part2Text)
if part2 and (part2:IsA("Model") or part2:IsA("BasePart")) then
selectedPart2 = part2
else
selectedPart2 = nil
end
UpdateInfo()
-- Remove old connections before creating new ones
if CFrameChangeConnection1 then CFrameChangeConnection1:Disconnect() end
if CFrameChangeConnection2 then CFrameChangeConnection2:Disconnect() end
if selectedPart1 and selectedPart1:IsA("BasePart") then
CFrameChangeConnection1 = selectedPart1:GetPropertyChangedSignal("CFrame"):Connect(UpdateInfo)
elseif selectedPart1 and selectedPart1:IsA("Model") then
CFrameChangeConnection1 = selectedPart1:GetPropertyChangedSignal("WorldPivot"):Connect(UpdateInfo)
end
if selectedPart2 and selectedPart2:IsA("BasePart") then
CFrameChangeConnection2 = selectedPart2:GetPropertyChangedSignal("CFrame"):Connect(UpdateInfo)
elseif selectedPart2 and selectedPart2:IsA("Model") then
CFrameChangeConnection2 = selectedPart2:GetPropertyChangedSignal("WorldPivot"):Connect(UpdateInfo)
end
end
Selection.SelectionChanged:Connect(function()
local selectedObjs = Selection:Get()
-- Only update parts if there are more than previously
if #selectedObjs > numOfSelectedParts then
SetSelection(selectedObjs[1], selectedObjs[2])
end
numOfSelectedParts = #selectedObjs
end)
-- Switch selected parts --
switchParts.MouseButton1Click:Connect(function()
-- Reverse parts
SetSelection(selectedPart2, selectedPart1)
end)
-- Edit --
pivotToPivot.MouseButton1Click:Connect(function()
local part2Pivot
if selectedPart2:IsA("Model") then
part2Pivot = selectedPart2.WorldPivot
else
part2Pivot = selectedPart2.CFrame * selectedPart2.PivotOffset
end
if selectedPart1:IsA("BasePart") then
-- selectedPart1.CFrame:Inverse() is equivalent to setting CFrame to 0,0,0; then add part 2 pivot
selectedPart1.PivotOffset = selectedPart1.CFrame:Inverse() * part2Pivot
elseif selectedPart1:IsA("Model") then
selectedPart1.WorldPivot = part2Pivot
end
UpdateInfo()
end)
CFrameToPivot.MouseButton1Click:Connect(function()
local part2Pivot
if selectedPart2:IsA("Model") then
part2Pivot = selectedPart2.WorldPivot
else
part2Pivot = selectedPart2.CFrame * selectedPart2.PivotOffset
end
selectedPart1:PivotTo(part2Pivot)
UpdateInfo()
end)
local function ValidateVector(vectorString: string)
-- Expect 3 params in a vector3
local params = {}
local index = 1
local function IndexNum()
local char = ""
local param = ""
repeat
char = string.sub(vectorString, index, index)
if char ~= "," and char ~= "" then
param ..= char
end
index += 1
until char == "," or char == "" or index > 1000 -- Emergency break
if tonumber(param) then return tonumber(param) end
return "InvalidParam"
end
for index2 = 1, 3 do
local numResult = IndexNum()
if numResult ~= "InvalidParam" then
params[index2] = numResult
else
return "Invalid"
end
end
if #params ~= 3 then return "Invalid" end
return params
end
local function GetVectors(vectorString1: string, vectorString2: string)
local function AddErrors(vector1Error: boolean, vector2Error: boolean)
errorText.Text = ""
if vector1Error then errorText.Text ..= "Vector #1 is invalid. " end
if vector2Error then errorText.Text ..= "Vector #2 is invalid. " end
end
local vector1
local vector2
local success, errorMessage = pcall(function()
vector1 = ValidateVector(vectorString1)
vector2 = ValidateVector(vectorString2)
end)
if not success then
warn("CFrameAssistant: Internal Vector Validation Error")
return "Error", "Error"
end
AddErrors(vector1 == "Invalid", vector2 == "Invalid")
if vector1 ~= "Invalid" and vector2 ~= "Invalid" then
return Vector3.new(vector1[1], vector1[2], vector1[3]), Vector3.new(vector2[1], vector2[2], vector2[3])
else
return "Error", "Error"
end
end
local function UpdateVectorInfo()
local vectorNum1String = vectorNum1Text.Text
local vectorNum2String = vectorNum2Text.Text
CheckTextFits(vectorNum1String, vectorNum1Text)
CheckTextFits(vectorNum2String, vectorNum2Text)
vectorNum1, vectorNum2 = GetVectors(vectorNum1String, vectorNum2String)
if vectorNum1 ~= "Error" then
local roundedAddVectors = RoundVector(vectorNum1 + vectorNum2, 3)
addVectors.Text = ConvertTableToString(roundedAddVectors)
local roundedSubtractVectors = RoundVector(vectorNum1 - vectorNum2, 3)
subtractVectors.Text = ConvertTableToString(roundedSubtractVectors)
local roundedMultiplyVectors = RoundVector(vectorNum1 * vectorNum2, 3)
multiplyVectors.Text = ConvertTableToString(roundedMultiplyVectors)
local roundedDivideVectors = RoundVector(vectorNum1 / vectorNum2, 3)
divideVectors.Text = ConvertTableToString(roundedDivideVectors)
end
end
-- Switch vectors
switchVectors.MouseButton1Click:Connect(function()
-- Reverse vectors
local currentVectorNum2Text = vectorNum2Text.Text
local currentVectorNum1Text = vectorNum1Text.Text
vectorNum1Text.Text = currentVectorNum2Text
vectorNum2Text.Text = currentVectorNum1Text
UpdateVectorInfo()
end)
-- New vector entered
vectorNum1Text.FocusLost:Connect(UpdateVectorInfo)
vectorNum2Text.FocusLost:Connect(UpdateVectorInfo)
-- On plugin enabled function --
pluginButton.Click:Connect(function()
widget.Enabled = not widget.Enabled
end)
Notes
This project was created just for a very simple purpose of finding the position between 2 vectors but obviously became much more complicated very quickly. There may be bugs in places I haven’t tested yet so please report them if you find any.
Download link: Click me!