How do I fix my rotation tool in my building game?

I am making a rotation tool in a building game I’m making, but I cannot get it to work right. Every solution I tried either stops it from rotating completely, or it jitters like crazy.

I tried:

  • Snapping rotation to ROTATION_INCREMENT (I needed to anyways) but that still causes it to jitter.
  • Adding a cumulativeAngles table to track X/Y/Z rotation.

Rotate Script:

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local CollectionService = game:GetService("CollectionService")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local ValidateMove = Remotes:WaitForChild("ValidateMove")

local placedBlocks = workspace:WaitForChild("PlacedBlocks")

local player = Players.LocalPlayer
local pgui = player:WaitForChild("PlayerGui")
local mouse = player:GetMouse()
local selectionBox = script.SelectionBox
local handles = script.RotateHandles
local hoverBox = script.SelectionBoxHover

local playerModule = player.PlayerScripts.PlayerModule
local CameraInput = require(playerModule.CameraModule.CameraInput)
local ControlModule = require(playerModule.ControlModule)

local module = {}

local enabled = false
local dragging = false
local currentHandles
local boundingPart
local originalBoundingCFrame
local modelOffsets = {}
local selectedModels = {}
local renderConn, mouseDownConn
local handleMouseDownConn, handleMouseUpConn, handleMouseDragConn
local shiftDownIgnoreArrowsFlag = false
local shiftConnBegan, shiftConnEnded
local DragConnection, guiDragConn, guiDragEndConn
local Corner1 = Vector2.zero
local validateDebounce = false

local ScaleGui = script.MoveGui
local GuiStarted = false

local lastCFrame

local lastTime = tick()

local cumulativeAngles = {X = 0, Y = 0, Z = 0}
local modelOffsets = {}

local movePresets = {
	90,
	45,
	15,
	5,
	1,
}
local ROTATE_INCREMENT = movePresets[2]

local modelSelectionBoxes = {}

local moveSelection = script.Selection_Rotate
local moveSelectionFrame = moveSelection:WaitForChild("Frame")

local moveLastValidText = ROTATE_INCREMENT
local typeMoveConn
local setMoveConn
local moveMinValue = 0.001
local moveMaxValue = 2

local function AngleFromAxis(axis, angle)
	local snapped = math.floor(angle / ROTATE_INCREMENT + 0.5) * ROTATE_INCREMENT

	local rotX, rotY, rotZ = 0, 0, 0
	if axis == Enum.Axis.X then
		rotX = math.rad(snapped)
	elseif axis == Enum.Axis.Y then
		rotY = math.rad(snapped)
	elseif axis == Enum.Axis.Z then
		rotZ = math.rad(snapped)
	end
	return {rotX, rotY, rotZ}
end

local function getScaledAmount(model, selectedModels, useVector, SetSize)
	if not model or not selectedModels then return 1 end

	local BlockId = model:GetAttribute("BLOCKID")
	if not BlockId then return 1 end

	local amount = 0
	for _, block in ipairs(selectedModels) do
		if block:GetAttribute("BLOCKID") == BlockId then
			local part = block:FindFirstChildWhichIsA("BasePart")
			if part then
				local size = part.Size
				if useVector and SetSize then
					if block == model then
						size = SetSize
					end
				end

				local blocksX = size.X / 2
				local blocksY = size.Y / 2
				local blocksZ = size.Z / 2
				amount += math.max(1, math.round(blocksX * blocksY * blocksZ))
			end
		end
	end

	return amount
end

local function formatSize(num)
	local rounded = math.floor(num * 1000 + 0.5) / 1000
	local str = string.format("%.3f", rounded)
	str = str:gsub("%.?0+$", "")
	return str
end

local function isValidNumber(text, minValue, maxValue)
	if not string.match(text, "^%d*%.?%d%d?%d?$") then
		return false
	end

	local num = tonumber(text)
	if not num then
		return false
	end

	return num >= minValue and num <= maxValue
end

local function getBiggestNumberFromSize(vec)
	local biggest = vec.X
	if vec.Y > biggest then biggest = vec.Y end
	if vec.Z > biggest then biggest = vec.Z end
	return biggest
end

local function initGui()
	if GuiStarted then return end
	GuiStarted = true

	if typeMoveConn then
		typeMoveConn:Disconnect()
		typeMoveConn = nil
	end
	if setMoveConn then
		setMoveConn:Disconnect()
		setMoveConn = nil
	end

	local frame = ScaleGui:FindFirstChild("Frame")
	if not frame then return end
	local increment = frame:FindFirstChild("Increment")
	if not increment then return end
	local MoveTextBox = increment:FindFirstChild("TextBox")
	if not MoveTextBox then return end
	local incrementButton = increment:FindFirstChild("Button")
	if not incrementButton then return end

	local function updateButtons()
		MoveTextBox.Text = ROTATE_INCREMENT
	end

	updateButtons()

	typeMoveConn = MoveTextBox.FocusLost:Connect(function()
		local text = MoveTextBox.Text

		if text == "" then
			MoveTextBox.Text = moveLastValidText
			return
		end

		if isValidNumber(text, moveMinValue, moveMaxValue) then
			moveLastValidText = text
			ROTATE_INCREMENT = tonumber(text)
		else
			MoveTextBox.Text = moveLastValidText
			ROTATE_INCREMENT = tonumber(moveLastValidText)
		end
	end)

	setMoveConn = incrementButton.MouseButton1Click:Connect(function()
		local defaultMoveIndex = 0
		for i, v in ipairs(movePresets) do
			if v == ROTATE_INCREMENT then
				defaultMoveIndex = i
				break
			end
		end

		defaultMoveIndex += 1
		if defaultMoveIndex > #movePresets then
			defaultMoveIndex = 1
		end

		ROTATE_INCREMENT = movePresets[defaultMoveIndex]
		MoveTextBox.Text = ROTATE_INCREMENT
	end)
end

local function updateGui(textOnly)
	if #selectedModels > 0 then
		ScaleGui.Parent = pgui

		local frame = ScaleGui:FindFirstChild("Frame")
		if not frame then return end
		local slot = frame:FindFirstChild("Slot")
		if not slot then return end
		local viewport = slot:FindFirstChild("BlockIcon")
		if not viewport then return end
		local amountText = slot:FindFirstChild("Amount")
		if not amountText then return end

		if not textOnly then
			for _, v in pairs(viewport:GetChildren()) do
				if v:IsA("Model") then
					v:Destroy()
				end
			end

			local firstName = selectedModels[1].Name
			local allSame = true
			for _, model in ipairs(selectedModels) do
				if model.Name ~= firstName then
					allSame = false
					break
				end
			end

			local viewportBlock
			if allSame then
				local replicatedBlock = game.ReplicatedStorage:WaitForChild("Blocks"):FindFirstChild(firstName)
				if replicatedBlock then
					viewportBlock = replicatedBlock:Clone()
				end
			else
				local templateBlock = game.ReplicatedStorage:WaitForChild("TemplateBlock")
				viewportBlock = templateBlock:Clone()
			end

			if viewportBlock then
				local orientation, size = viewportBlock:GetBoundingBox()
				local cameraDistance = getBiggestNumberFromSize(size) * 1.5
				viewportBlock:PivotTo(CFrame.new(0,0,cameraDistance) * CFrame.Angles(0, math.rad(45), math.rad(45)))
				viewportBlock.Parent = viewport
			end
		end

		local userFolder = placedBlocks:FindFirstChild(player.UserId)
		if userFolder then
			local countedBlocks = {}
			local totalAmount = 0

			for _, model in ipairs(selectedModels) do
				local blockName = model.Name
				if not countedBlocks[blockName] then
					local amt = getScaledAmount(model, selectedModels)
					countedBlocks[blockName] = amt
					totalAmount += amt
				end
			end

			amountText.Text = tostring(totalAmount)
		end
	else
		ScaleGui.Parent = script
	end
end

local function clearSelection()
	if validateDebounce then return end
	selectedModels = {}
	if boundingPart then
		boundingPart:Destroy()
		boundingPart = nil
	end
	if handleMouseDownConn then handleMouseDownConn:Disconnect() handleMouseDownConn = nil end
	if handleMouseUpConn then handleMouseUpConn:Disconnect() handleMouseUpConn = nil end
	if handleMouseDragConn then handleMouseDragConn:Disconnect() handleMouseDragConn = nil end
	if currentHandles then
		currentHandles:Destroy()
		currentHandles = nil
	end
	selectionBox.Parent = script
	selectionBox.Adornee = nil
	hoverBox.Parent = script
	hoverBox.Adornee = nil

	for model, box in pairs(modelSelectionBoxes) do
		if box then
			box:Destroy()
		end
	end
	modelSelectionBoxes = {}
end

local function getBoundingBox(models)
	if #models == 0 then return nil, nil end
	local minVec, maxVec
	for _, model in ipairs(models) do
		for _, part in ipairs(model:GetDescendants()) do
			if part:IsA("BasePart") then
				local cf = part.CFrame
				local size = part.Size / 2
				local corners = {
					cf * Vector3.new(-size.X,-size.Y,-size.Z),
					cf * Vector3.new(-size.X,-size.Y, size.Z),
					cf * Vector3.new(-size.X, size.Y,-size.Z),
					cf * Vector3.new(-size.X, size.Y, size.Z),
					cf * Vector3.new( size.X,-size.Y,-size.Z),
					cf * Vector3.new( size.X,-size.Y, size.Z),
					cf * Vector3.new( size.X, size.Y,-size.Z),
					cf * Vector3.new( size.X, size.Y, size.Z),
				}
				for _, corner in ipairs(corners) do
					if not minVec then
						minVec = corner
						maxVec = corner
					else
						minVec = Vector3.new(math.min(minVec.X, corner.X), math.min(minVec.Y, corner.Y), math.min(minVec.Z, corner.Z))
						maxVec = Vector3.new(math.max(maxVec.X, corner.X), math.max(maxVec.Y, corner.Y), math.max(maxVec.Z, corner.Z))
					end
				end
			end
		end
	end
	if not minVec or not maxVec then return nil, nil end
	return CFrame.new((minVec + maxVec) / 2), maxVec - minVec
end

local function updateBoundingPart()
	if #selectedModels == 0 then
		if boundingPart then boundingPart:Destroy() end
		boundingPart = nil
		selectionBox.Adornee = nil

		if currentHandles then
			handleMouseDownConn:Disconnect() handleMouseDownConn = nil
			handleMouseUpConn:Disconnect() handleMouseUpConn = nil
			handleMouseDragConn:Disconnect() handleMouseDragConn = nil
			currentHandles:Destroy()
			currentHandles = nil
		end

		return
	end

	local cf, size = getBoundingBox(selectedModels)
	if not cf or not size then return end
	if not boundingPart then
		boundingPart = Instance.new("Part")
		boundingPart.CanQuery = false
		boundingPart.Anchored = true
		boundingPart.Transparency = 1
		boundingPart.CanCollide = false
		boundingPart.Name = "MoveBoundingPart"
		boundingPart.Parent = workspace
		selectionBox.Parent = workspace
		selectionBox.Adornee = boundingPart
	end
	boundingPart.CFrame = cf
	boundingPart.Size = size
end

local function toggleSelection(model, shift, touchEnabled)
	if validateDebounce then return end
	if #selectedModels > 1 and not shift and not touchEnabled then
		clearSelection()
		toggleSelection(model)
		return
	end

	local idx = table.find(selectedModels, model)
	if idx then
		table.remove(selectedModels, idx)
		if modelSelectionBoxes[model] then
			modelSelectionBoxes[model]:Destroy()
			modelSelectionBoxes[model] = nil
		end
	else
		table.insert(selectedModels, model)
		local box = selectionBox:Clone()
		box.Adornee = model
		box.Parent = workspace
		modelSelectionBoxes[model] = box
	end
	updateBoundingPart()
	updateGui()
end

local function beginRotate(axis)
	if not boundingPart or validateDebounce or shiftDownIgnoreArrowsFlag then return end
	dragging = true
	originalBoundingCFrame = boundingPart.CFrame

	local pivot = originalBoundingCFrame.Position
	modelOffsets = {}
	for _, model in ipairs(selectedModels) do
		if model.PrimaryPart then
			local relPos = model.PrimaryPart.Position - pivot
			local relCFrame = CFrame.new(relPos):ToObjectSpace(originalBoundingCFrame:inverse() * model.PrimaryPart.CFrame)
			modelOffsets[model] = {PositionOffset = relPos, OrientationOffset = model.PrimaryPart.CFrame - CFrame.new(model.PrimaryPart.Position)}
		end
	end

	cumulativeAngles = {X=0, Y=0, Z=0}
end

local function endRotate()
	if not boundingPart or not dragging then return end
	dragging = false
	validateDebounce = true

	local rotations = {}
	for model, offset in pairs(modelOffsets) do
		if model.PrimaryPart then
			table.insert(rotations, {Model = model, CFrame = boundingPart.CFrame * offset})
		end
	end

	local result = ValidateMove:InvokeServer(rotations)
	if not result or not result.Success then
		for model, offset in pairs(modelOffsets) do
			if model.PrimaryPart then
				model:SetPrimaryPartCFrame(originalBoundingCFrame * offset)
			end
		end
		boundingPart.CFrame = originalBoundingCFrame
		script.Reject:Play()
	end

	modelOffsets = {}
	validateDebounce = false
end
local function rotateBounding(axis, relativeAngle, deltaRadius)
	if not boundingPart or not dragging or validateDebounce then return end

	if axis == Enum.Axis.X then
		cumulativeAngles.X = cumulativeAngles.X + relativeAngle
	elseif axis == Enum.Axis.Y then
		cumulativeAngles.Y = cumulativeAngles.Y + relativeAngle
	elseif axis == Enum.Axis.Z then
		cumulativeAngles.Z = cumulativeAngles.Z + relativeAngle
	else
		return
	end

	local snapX = math.floor(cumulativeAngles.X / ROTATE_INCREMENT + 0.5) * ROTATE_INCREMENT
	local snapY = math.floor(cumulativeAngles.Y / ROTATE_INCREMENT + 0.5) * ROTATE_INCREMENT
	local snapZ = math.floor(cumulativeAngles.Z / ROTATE_INCREMENT + 0.5) * ROTATE_INCREMENT

	local pivot = originalBoundingCFrame.Position
	local rotationCFrame = CFrame.Angles(math.rad(snapX), math.rad(snapY), math.rad(snapZ))
	local newCFrame = CFrame.new(pivot) * rotationCFrame

	boundingPart.CFrame = newCFrame

	for model, offset in pairs(modelOffsets) do
		if model.PrimaryPart then
			model:SetPrimaryPartCFrame(newCFrame * offset)
		end
	end
end

local function updateHover()
	if UserInputService.TouchEnabled then return end

	if not mouse.Target then
		hoverBox.Adornee = nil
		return
	end
	local target = mouse.Target
	local model = target:FindFirstAncestorOfClass("Model")
	if model and CollectionService:HasTag(model, "Scalable") then
		if model:IsDescendantOf(workspace.PlacedBlocks:FindFirstChild(player.UserId)) then
			hoverBox.Parent = workspace
			hoverBox.Adornee = model.PrimaryPart or target
		else
			hoverBox.Adornee = nil
		end
	else
		hoverBox.Adornee = nil
	end
end

local function updateHandlesParent()
	if currentHandles then
		if shiftDownIgnoreArrowsFlag then
			currentHandles.Parent = workspace
		else
			currentHandles.Parent = pgui
		end
	end
end

local function onClick()
	if validateDebounce then return end
	if not mouse.Target then return end
	local model = mouse.Target:FindFirstAncestorOfClass("Model")
	if not model or not CollectionService:HasTag(model, "Scalable") then return end
	if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) or UserInputService:IsKeyDown(Enum.KeyCode.RightShift) or UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or UserInputService:IsKeyDown(Enum.KeyCode.RightControl) or UserInputService.TouchEnabled then
		toggleSelection(model, true, UserInputService.TouchEnabled)
	else
		if not table.find(selectedModels, model) then
			clearSelection()
		end
		toggleSelection(model, false, UserInputService.TouchEnabled)
	end
	if boundingPart then
		if currentHandles then
			handleMouseDownConn:Disconnect() handleMouseDownConn = nil
			handleMouseUpConn:Disconnect() handleMouseUpConn = nil
			handleMouseDragConn:Disconnect() handleMouseDragConn = nil
			currentHandles:Destroy()
			currentHandles = nil
		end
		currentHandles = handles:Clone()
		currentHandles.Adornee = boundingPart
		updateHandlesParent()
		
		handleMouseDownConn = currentHandles.MouseButton1Down:Connect(function(axis)
			beginRotate(axis)
		end)

		handleMouseDragConn = currentHandles.MouseDrag:Connect(function(axis, relativeAngle, deltaRadius)
			rotateBounding(axis, relativeAngle, deltaRadius)
		end)

		handleMouseUpConn = currentHandles.MouseButton1Up:Connect(endRotate)
	end
end

function module.Enable()
	if enabled then return end
	enabled = true
	if renderConn then renderConn:Disconnect() renderConn = nil end
	if mouseDownConn then mouseDownConn:Disconnect() mouseDownConn = nil end
	renderConn = RunService.RenderStepped:Connect(updateHover)
	mouseDownConn = mouse.Button1Down:Connect(onClick)
	shiftConnBegan = UserInputService.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.Keyboard and (input.KeyCode == Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.RightShift or input.KeyCode == Enum.KeyCode.LeftControl or input.KeyCode == Enum.KeyCode.RightControl) then
			shiftDownIgnoreArrowsFlag = true
			updateHandlesParent()
		end
	end)
	shiftConnEnded = UserInputService.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.Keyboard and (input.KeyCode == Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.RightShift or input.KeyCode == Enum.KeyCode.LeftControl or input.KeyCode == Enum.KeyCode.RightControl) then
			shiftDownIgnoreArrowsFlag = false
			updateHandlesParent()
		end
	end)

	local MIN_DRAG_DISTANCE = 5
	local DraggingActive = false
	local Corner1 = Vector2.new()

	local tempModelSelect = {}

	guiDragConn = UserInputService.InputBegan:Connect(function(input, gameProcessed)
		if input.UserInputType ~= Enum.UserInputType.MouseButton1 or gameProcessed then return end
		Corner1 = Vector2.new(mouse.X, mouse.Y)
		DraggingActive = false

		if DragConnection then DragConnection:Disconnect() end
		DragConnection = UserInputService.InputChanged:Connect(function(inputObject)
			if inputObject.UserInputType ~= Enum.UserInputType.MouseMovement then return end
			local currentPos = Vector2.new(mouse.X, mouse.Y)
			local delta = (currentPos - Corner1).magnitude

			if not DraggingActive and delta >= MIN_DRAG_DISTANCE then
				DraggingActive = true
				moveSelection.Parent = pgui
				moveSelectionFrame.Position = UDim2.fromOffset(Corner1.X, Corner1.Y)
				moveSelectionFrame.Size = UDim2.fromOffset(0, 0)
				clearSelection()
			end

			if DraggingActive then
				local size = currentPos - Corner1
				local pos = Corner1 + Vector2.new(math.min(0, size.X), math.min(0, size.Y))
				local absSize = Vector2.new(math.abs(size.X), math.abs(size.Y))
				moveSelectionFrame.Position = UDim2.fromOffset(pos.X, pos.Y)
				moveSelectionFrame.Size = UDim2.fromOffset(absSize.X, absSize.Y)

				local userFolder = placedBlocks:FindFirstChild(player.UserId)
				if not userFolder then return end

				for _, model in ipairs(userFolder:GetChildren()) do
					if model:IsA("Model") and CollectionService:HasTag(model, "Scalable") and model.PrimaryPart then
						local screenPos, onScreen = workspace.CurrentCamera:WorldToViewportPoint(model.PrimaryPart.Position)
						if onScreen then
							local pos2D = Vector2.new(screenPos.X, screenPos.Y)
							local inRect = pos2D.X >= pos.X and pos2D.X <= pos.X + absSize.X and
								pos2D.Y >= pos.Y and pos2D.Y <= pos.Y + absSize.Y
							if inRect then
								if not modelSelectionBoxes[model] then
									local box = selectionBox:Clone()
									box.Adornee = model
									box.Parent = workspace
									modelSelectionBoxes[model] = box
								end
							else
								if modelSelectionBoxes[model] then
									modelSelectionBoxes[model]:Destroy()
									modelSelectionBoxes[model] = nil
								end
							end
						end
					end
				end
			end
		end)
	end)

	guiDragEndConn = UserInputService.InputEnded:Connect(function(input, gameProcessed)
		if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
		moveSelection.Parent = script
		if DragConnection then
			DragConnection:Disconnect()
			DragConnection = nil
		end
		if not DraggingActive then return end
		DraggingActive = false

		local corner2 = Vector2.new(mouse.X, mouse.Y)
		local size = corner2 - Corner1
		local negate = Vector2.new(size.X - math.abs(size.X), size.Y - math.abs(size.Y)) / 2
		local adjustedCorner1 = Corner1 + negate
		local adjustedCorner2 = corner2 - negate

		local userFolder = placedBlocks:FindFirstChild(player.UserId)
		if not userFolder then return end

		local topLeft = Vector2.new(math.min(adjustedCorner1.X, adjustedCorner2.X), math.min(adjustedCorner1.Y, adjustedCorner2.Y))
		local bottomRight = Vector2.new(math.max(adjustedCorner1.X, adjustedCorner2.X), math.max(adjustedCorner1.Y, adjustedCorner2.Y))

		clearSelection()

		local selectedTotal = 0
		for _, model in ipairs(userFolder:GetChildren()) do
			if model:IsA("Model") and CollectionService:HasTag(model, "Scalable") and model.PrimaryPart then
				local minScreen, maxScreen
				for _, part in ipairs(model:GetDescendants()) do
					if part:IsA("BasePart") then
						local corners = {
							part.CFrame * Vector3.new(-part.Size.X/2, -part.Size.Y/2, -part.Size.Z/2),
							part.CFrame * Vector3.new(-part.Size.X/2, -part.Size.Y/2, part.Size.Z/2),
							part.CFrame * Vector3.new(-part.Size.X/2, part.Size.Y/2, -part.Size.Z/2),
							part.CFrame * Vector3.new(-part.Size.X/2, part.Size.Y/2, part.Size.Z/2),
							part.CFrame * Vector3.new(part.Size.X/2, -part.Size.Y/2, -part.Size.Z/2),
							part.CFrame * Vector3.new(part.Size.X/2, -part.Size.Y/2, part.Size.Z/2),
							part.CFrame * Vector3.new(part.Size.X/2, part.Size.Y/2, -part.Size.Z/2),
							part.CFrame * Vector3.new(part.Size.X/2, part.Size.Y/2, part.Size.Z/2),
						}
						for _, corner in ipairs(corners) do
							local screenPos, onScreen = workspace.CurrentCamera:WorldToViewportPoint(corner)
							if onScreen then
								local pos2D = Vector2.new(screenPos.X, screenPos.Y)
								if not minScreen then
									minScreen = pos2D
									maxScreen = pos2D
								else
									minScreen = Vector2.new(math.min(minScreen.X, pos2D.X), math.min(minScreen.Y, pos2D.Y))
									maxScreen = Vector2.new(math.max(maxScreen.X, pos2D.X), math.max(maxScreen.Y, pos2D.Y))
								end
							end
						end
					end
				end
				if minScreen and maxScreen then
					if maxScreen.X >= topLeft.X and minScreen.X <= bottomRight.X and
						maxScreen.Y >= topLeft.Y and minScreen.Y <= bottomRight.Y then
						selectedTotal += 1
						toggleSelection(model, true, UserInputService.TouchEnabled)
					end
				end
			end
			if selectedTotal <= 0 then
				ScaleGui.Parent = script
			end
			tempModelSelect = {}
		end

		updateBoundingPart()

		if boundingPart then
			if currentHandles then
				handleMouseDownConn:Disconnect()
				handleMouseUpConn:Disconnect()
				handleMouseDragConn:Disconnect()
				currentHandles:Destroy()
				currentHandles = nil
			end
			currentHandles = handles:Clone()
			currentHandles.Adornee = boundingPart
			updateHandlesParent()
			handleMouseDownConn = currentHandles.MouseButton1Down:Connect(beginRotate)
			handleMouseUpConn = currentHandles.MouseButton1Up:Connect(endRotate)
			handleMouseDragConn = currentHandles.MouseDrag:Connect(rotateBounding)
		end
	end)

	initGui()
end

function module.Disable()
	if not enabled then return end
	enabled = false

	if renderConn then renderConn:Disconnect() renderConn = nil end
	if mouseDownConn then mouseDownConn:Disconnect() mouseDownConn = nil end
	if shiftConnBegan then shiftConnBegan:Disconnect() shiftConnBegan = nil end
	if shiftConnEnded then shiftConnEnded:Disconnect() shiftConnEnded = nil end
	if typeMoveConn then typeMoveConn:Disconnect() typeMoveConn = nil end
	if setMoveConn then setMoveConn:Disconnect() setMoveConn = nil end

	shiftDownIgnoreArrowsFlag = false
	validateDebounce = false
	dragging = false

	if handleMouseDownConn then handleMouseDownConn:Disconnect() handleMouseDownConn = nil end
	if handleMouseUpConn then handleMouseUpConn:Disconnect() handleMouseUpConn = nil end
	if handleMouseDragConn then handleMouseDragConn:Disconnect() handleMouseDragConn = nil end
	if guiDragConn then guiDragConn:Disconnect() guiDragConn = nil end
	if guiDragEndConn then guiDragEndConn:Disconnect() guiDragEndConn = nil end
	if DragConnection then DragConnection:Disconnect() DragConnection = nil end

	moveSelection.Parent = script
	ScaleGui.Parent = script

	clearSelection()

	if currentHandles then
		currentHandles:Destroy()
		currentHandles = nil
	end
	if boundingPart then
		boundingPart:Destroy()
		boundingPart = nil
	end
	selectedModels = {}
	selectionBox.Parent = script
	selectionBox.Adornee = nil
	hoverBox.Parent = script
	hoverBox.Adornee = nil

	for model, box in pairs(modelSelectionBoxes) do
		if box then
			box:Destroy()
		end
	end
	modelSelectionBoxes = {}
	
	CameraInput.setInputEnabled(true)
	ControlModule:Enable()
end

return module

Any help is appreciated since I don’t know any way to fix it myself