CFrame Assistant (Plugin)

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.

Donate

If you would like to support the creation of new free, open-source projects like this, you can contribute (robux only) at this link.

Download link: Click me!

1 Like

that rotation difference is going to be a gamechanger for me. thanks bro!

1 Like

Update: 8/15/2025

You’re probably asking “Why would someone bump a 6 month old post and just now decide to do an update?”

Because I can.

Anyways new update today yay.


There’s a new button under the edit section called “Randomize part 1 rotation”. And it does exactly what you think it does. It randomizes the part 1’s rotation. It also works for models.


The way part selection works is now improved too. It’s pretty complicated to explain so I guess that’s it.


If anyone still uses this you can update your plugin to get the latest version from the “manage plugins” tab/button.