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.

Download link: Click me!

1 Like

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

1 Like