How do I mirror these parts correctly?

I made a mirror selection tool in a building game I’m making. Everything works correctly when rotated at 90 degrees, but it doesn’t mirror correctly when rotated.


Here I mirror 5 blocks rotated at 45 degrees. It creates a V shape instead of what should happen in the example below.


When I clone 5 blocks in Build A Boat For Treasure, it stacks them ontop of each other. This is what I’m trying to achieve.

I added a print for the positions variable before firing the remove function and it prints this:
["CFrame"] = 106, 22.1715851, 459.828461, 1, 0, 0, 0, 0.707134247, 0.707079291, 0,

Mirror tool script (client)

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 ValidateMirror = Remotes:WaitForChild("ValidateMirror")

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.MoveHandles
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 = require(script.Parent.Parent.GlobalFunctions.SelectedModels)
local renderConn, mouseDownConn
local handleMouseDownConn, handleMouseUpConn
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 currentFace
local currentDistance

local currentFace

local modelSelectionBoxes = {}

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

local selectionPool = {}
local function getSelectionVisual()
	if #selectionPool > 0 then
		local v = table.remove(selectionPool)
		v.Parent = workspace
		return v
	end
	local box = selectionBox:Clone()
	box.Parent = workspace
	return box
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 updateGui(textOnly)
	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 #selectedModels.Selected > 0 then
		amountText.Visible = true
		
		if not textOnly then
			for _, v in pairs(viewport:GetChildren()) do
				if v:IsA("Model") then
					v:Destroy()
				end
			end

			local firstName = selectedModels.Selected[1].Name
			local allSame = true
			for _, model in ipairs(selectedModels.Selected) 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.Selected) do
				local blockName = model.Name
				if not countedBlocks[blockName] then
					local amt = getScaledAmount(model, selectedModels.Selected)
					countedBlocks[blockName] = amt
					totalAmount += amt
				end
			end

			amountText.Text = tostring(totalAmount)
		end
	else
		amountText.Visible = false
		for _, v in pairs(viewport:GetChildren()) do
			if v:IsA("Model") then
				v:Destroy()
			end
		end
	end
end

local function clearSelection()
	if validateDebounce then return end
	selectedModels.Selected = {}
	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 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 = {}
	updateGui()
end

local function getBoundingBox(models)
	if #models == 0 then return nil, nil end

	local allCorners = {}
	local lastModelPivot
	for i, model in ipairs(models) do
		local success, cf, size = pcall(function() return model:GetBoundingBox() end)
		if success and cf and size then
			local halfSize = size / 2
			local corners = {
				cf * Vector3.new(-halfSize.X,-halfSize.Y,-halfSize.Z),
				cf * Vector3.new(-halfSize.X,-halfSize.Y, halfSize.Z),
				cf * Vector3.new(-halfSize.X, halfSize.Y,-halfSize.Z),
				cf * Vector3.new(-halfSize.X, halfSize.Y, halfSize.Z),
				cf * Vector3.new( halfSize.X,-halfSize.Y,-halfSize.Z),
				cf * Vector3.new( halfSize.X,-halfSize.Y, halfSize.Z),
				cf * Vector3.new( halfSize.X, halfSize.Y,-halfSize.Z),
				cf * Vector3.new( halfSize.X, halfSize.Y, halfSize.Z),
			}
			for _, corner in ipairs(corners) do
				table.insert(allCorners, corner)
			end
			if i == #models then
				local successPivot, pivot = pcall(function() return model:GetPivot() end)
				lastModelPivot = (successPivot and pivot) or cf
			end
		end
	end

	if #allCorners == 0 then return nil, nil end

	local invPivot = lastModelPivot:Inverse()
	local localCorners = {}
	for _, corner in ipairs(allCorners) do
		table.insert(localCorners, invPivot * corner)
	end

	local minVec = localCorners[1]
	local maxVec = localCorners[1]
	for _, corner in ipairs(localCorners) do
		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

	local centerLocal = (minVec + maxVec) / 2
	local size = maxVec - minVec
	local boundingCFrame = lastModelPivot * CFrame.new(centerLocal)

	return boundingCFrame, size
end

local function updateBoundingPart()
	if #selectedModels.Selected == 0 then
		if boundingPart then boundingPart:Destroy() end
		boundingPart = nil
		selectionBox.Adornee = nil
		if currentHandles then
			handleMouseDownConn:Disconnect() handleMouseDownConn = nil
			handleMouseUpConn:Disconnect() handleMouseUpConn = nil
			currentHandles:Destroy()
			currentHandles = nil
		end
		return
	end

	local cf, size = getBoundingBox(selectedModels.Selected)
	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)
	local owner = model:GetAttribute("Owner")
	if owner ~= player.UserId then return end

	if validateDebounce then return end
	if #selectedModels.Selected > 1 and not shift and not touchEnabled then
		clearSelection()
		toggleSelection(model)
		return
	end

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


local function beginMove()
	if not boundingPart or validateDebounce or shiftDownIgnoreArrowsFlag then return end
	dragging = true

	local boundingBoxCFrame = boundingPart.CFrame
	if not boundingBoxCFrame then
		warn("BoundingPart has no CFrame")
		dragging = false
		return
	end
	originalBoundingCFrame = boundingBoxCFrame
	modelOffsets = {}

	for _, model in ipairs(selectedModels.Selected) do
		if model and model:IsA("Model") then
			local modelCFrame, _ = model:GetBoundingBox()
			if modelCFrame then
				modelOffsets[model] = originalBoundingCFrame:ToObjectSpace(modelCFrame)
			else
				local primary = model.PrimaryPart
				if primary then
					modelOffsets[model] = originalBoundingCFrame:ToObjectSpace(primary.CFrame)
				else
					warn("Model has no parts or bounding box:", model:GetFullName())
				end
			end
		end
	end

	if UserInputService.TouchEnabled then
		CameraInput.setInputEnabled(false)
		ControlModule:Disable()
	end
end

local function selectNewBlocks(newBlocks)
	for _, box in pairs(modelSelectionBoxes) do
		if box and box.Parent then
			box:Destroy()
		end
	end
	modelSelectionBoxes = {}

	selectedModels.Selected = {}

	if enabled == true then
		for _, model in ipairs(newBlocks) do
			table.insert(selectedModels.Selected, model)
			local box = selectionBox:Clone()
			box.Adornee = model.PrimaryPart or model
			box.Parent = workspace
			modelSelectionBoxes[model] = box
		end

		updateBoundingPart()

		modelOffsets = {}
		if boundingPart then
			for _, model in ipairs(selectedModels.Selected) do
				if model.PrimaryPart then
					modelOffsets[model] = boundingPart.CFrame:ToObjectSpace(model.PrimaryPart.CFrame)
				end
			end
		end

		updateGui()
	end
end

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

	CameraInput.setInputEnabled(true)
	ControlModule:Enable()
	
	local positions = {}
	for model, offset in pairs(modelOffsets) do
		if typeof(offset) == "CFrame" and boundingPart and boundingPart.CFrame then
			table.insert(positions, {Model = model, CFrame = boundingPart.CFrame * offset})
		else
			if model and model:IsA("Model") then
				local success, modelCFrame = pcall(function() return model:GetBoundingBox() end)
				if success and modelCFrame and originalBoundingCFrame then
					local fallbackOffset = originalBoundingCFrame:ToObjectSpace(modelCFrame)
					if typeof(fallbackOffset) == "CFrame" and boundingPart and boundingPart.CFrame then
						table.insert(positions, {Model = model, CFrame = boundingPart.CFrame * fallbackOffset})
					end
				end
			end
		end
	end
	
	local result
	pcall(function()
		print(positions)
		result = ValidateMirror:InvokeServer(positions, currentFace)
	end)

	if not result or not result.Success then
		if originalBoundingCFrame then
			boundingPart.CFrame = originalBoundingCFrame
		end
		script.Reject:Play()
	end

	if result and result.newBlocks then
		selectNewBlocks(result.newBlocks)
	end

	validateDebounce = false
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 then
		local owner = model:GetAttribute("Owner")
		if model:IsDescendantOf(workspace.PlacedBlocks:FindFirstChild(player.UserId)) and owner == 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
		currentHandles.Parent = shiftDownIgnoreArrowsFlag and workspace or pgui
	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 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.Selected, model) then
			clearSelection()
		end
		toggleSelection(model, false, UserInputService.TouchEnabled)
	end
	if boundingPart then
		if currentHandles then
			handleMouseDownConn:Disconnect() handleMouseDownConn = nil
			handleMouseUpConn:Disconnect() handleMouseUpConn = nil
			currentHandles:Destroy()
			currentHandles = nil
		end
		currentHandles = handles:Clone()
		currentHandles.Adornee = boundingPart
		currentHandles.Style = Enum.HandlesStyle.Movement
		updateHandlesParent()
		handleMouseDownConn = currentHandles.MouseButton1Down:Connect(beginMove)
		handleMouseUpConn = currentHandles.MouseButton1Up:Connect(function(face)
			currentFace = face
			endMove()
		end)
	end
end

local function selectCurrentModels()
	if not enabled then return end
	if not selectedModels.Selected then return end

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

	for i = #selectedModels.Selected, 1, -1 do
		local model = selectedModels.Selected[i]
		if not model or not model:IsDescendantOf(workspace) then
			table.remove(selectedModels.Selected, i)
		end
	end

	for _, model in ipairs(selectedModels.Selected) do
		local box = selectionBox:Clone()
		box.Adornee = model.PrimaryPart or model
		box.Parent = workspace
		modelSelectionBoxes[model] = box
	end

	updateBoundingPart()

	if boundingPart and boundingPart:IsDescendantOf(workspace) then
		if currentHandles then
			if handleMouseDownConn then handleMouseDownConn:Disconnect() end
			if handleMouseUpConn then handleMouseUpConn:Disconnect() end
			currentHandles:Destroy()
			currentHandles = nil
		end

		currentHandles = handles:Clone()
		currentHandles.Adornee = boundingPart
		currentHandles.Style = Enum.HandlesStyle.Movement
		updateHandlesParent()

		handleMouseDownConn = currentHandles.MouseButton1Down:Connect(beginMove)
		handleMouseUpConn = currentHandles.MouseButton1Up:Connect(function(face)
			currentFace = face
			endMove()
		end)
	end

	updateGui()
end

function module.Enable()
	if enabled then return end
	enabled = true
	GuiStarted = false

	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) -- raw mouse pos
		DraggingActive = false

		local guiInset = game:GetService("GuiService"):GetGuiInset()

		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) -- raw mouse pos
			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)
				if not UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) and not UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) and not UserInputService:IsKeyDown(Enum.KeyCode.RightShift) and not UserInputService:IsKeyDown(Enum.KeyCode.RightControl) then
					clearSelection()
				end
			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
					local owner = model:GetAttribute("Owner")
					if model:IsA("Model") and owner == player.UserId then
						local cf, size = model:GetBoundingBox()
						local corners = {
							cf * Vector3.new(-size.X/2, -size.Y/2, -size.Z/2),
							cf * Vector3.new(-size.X/2, -size.Y/2,  size.Z/2),
							cf * Vector3.new(-size.X/2,  size.Y/2, -size.Z/2),
							cf * Vector3.new(-size.X/2,  size.Y/2,  size.Z/2),
							cf * Vector3.new( size.X/2, -size.Y/2, -size.Z/2),
							cf * Vector3.new( size.X/2, -size.Y/2,  size.Z/2),
							cf * Vector3.new( size.X/2,  size.Y/2, -size.Z/2),
							cf * Vector3.new( size.X/2,  size.Y/2,  size.Z/2),
						}

						local minScreen, maxScreen
						for _, corner in ipairs(corners) do
							local screenPos, onScreen = workspace.CurrentCamera:WorldToViewportPoint(corner)
							if onScreen and screenPos.Z > 0 then
								local pos2D = Vector2.new(screenPos.X, screenPos.Y - guiInset.Y)
								if not minScreen then
									minScreen, maxScreen = pos2D, 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

						if minScreen and maxScreen then
							local inRect = maxScreen.X >= pos.X and minScreen.X <= pos.X + absSize.X and
								maxScreen.Y >= pos.Y and minScreen.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] and not table.find(selectedModels.Selected, model) then
									modelSelectionBoxes[model]:Destroy()
									modelSelectionBoxes[model] = nil
								end
							end
						end
					end
				end
			end
		end)
	end)

	guiDragEndConn = UserInputService.InputEnded:Connect(function(input, gameProcessed)
		if not (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
			return
		end

		moveSelection.Parent = script

		if DragConnection then
			DragConnection:Disconnect()
			DragConnection = nil
		end

		if not DraggingActive then
			return
		end
		DraggingActive = false

		local finalModels = {}
		for model, box in pairs(modelSelectionBoxes) do
			if model and typeof(model) == "Instance" and model:IsA("Model") and box then
				table.insert(finalModels, model)
			end
		end

		local modifierDown = UserInputService:IsKeyDown(Enum.KeyCode.LeftShift)
			or UserInputService:IsKeyDown(Enum.KeyCode.RightShift)
			or UserInputService:IsKeyDown(Enum.KeyCode.LeftControl)
			or UserInputService:IsKeyDown(Enum.KeyCode.RightControl)

		if not modifierDown then
			clearSelection()
		end

		for _, model in ipairs(finalModels) do
			if model and model:IsDescendantOf(workspace) and model:GetAttribute("Owner") == player.UserId then
				if not table.find(selectedModels.Selected, model) then
					table.insert(selectedModels.Selected, model)
				end

				if not modelSelectionBoxes[model] then
					local box = getSelectionVisual()
					box.Adornee = model.PrimaryPart or model
					modelSelectionBoxes[model] = box
				end
			end
		end

		updateBoundingPart()
		updateGui()

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

			currentHandles = handles:Clone()
			currentHandles.Adornee = boundingPart
			currentHandles.Style = Enum.HandlesStyle.Movement
			updateHandlesParent()
			handleMouseDownConn = currentHandles.MouseButton1Down:Connect(beginMove)
			handleMouseUpConn = currentHandles.MouseButton1Up:Connect(function(face)
				currentFace = face
				endMove()
			end)
		end
	end)

	selectCurrentModels()
	
	updateGui()
	ScaleGui.Parent = pgui
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

	shiftDownIgnoreArrowsFlag = false
	--validateDebounce = false
	dragging = false

	if handleMouseDownConn then handleMouseDownConn:Disconnect() handleMouseDownConn = nil end
	if handleMouseUpConn then handleMouseUpConn:Disconnect() handleMouseUpConn = 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 have a really hard time understanding CFrames :blowfish:

1 Like

I think you’ve pasted a bit too much of your code for someone to reasonably go through and make the adjustment for you.

Instead, I’ll opt to try and explain the CFrame math to you and hopefully you can implement it yourself.


From your videos it seems like we can assume we’re cloning multiple uniform cube parts in the direction the dragger is clicked. We want to know for each cube how can we figure out the direction of translation that aligns with the dragger arrow.

Let’s assume you already have the NormalId from the dragger. We can can convert this into a Vector3 with Vector3.FromNormalId.

local normal = Vector3.FromNormalId(handleNormalId)

This vector however is not representative of where the handle is actually pointing though. Instead, it represents the object space direction of the face i.e. (0, 1, 0) is up, (1, 0, 0) is right, etc. So, we need to convert it to world space.

local worldNormal = handle.Adornee.CFrame:VectorToWorldSpace(normal)

From there you can then translate the original CFrame with the world space direction:

local copy = cube:Clone()
copy.CFrame = cube.CFrame + worldNormal * cube.Size.X / 2 -- assuming cube uniform
copy.Parent = cube.Parent
-- etc...

Good luck!

3 Likes

Okay, I’m getting there. I have 2 issues though.

1. The bounding part only moves 1 block length.

2. The server function that handles mirroring sets it back to the wrong positions

The bounding box’s rotation is correct but the server script overrides it.
And also, when I select 2 blocks it only moves it by 1 block.

Here is what I got for the bounding part in the client

local positions = {}
	for model, offset in pairs(modelOffsets) do
		if typeof(offset) == "CFrame" and boundingPart and boundingPart.CFrame then
			table.insert(positions, {Model = model, CFrame = boundingPart.CFrame * offset})
		else
			if model and model:IsA("Model") then
				local success, modelCFrame = pcall(function()
					return model:GetBoundingBox()
				end)
				if success and modelCFrame and originalBoundingCFrame then
					local fallbackOffset = originalBoundingCFrame:ToObjectSpace(modelCFrame)
					if typeof(fallbackOffset) == "CFrame" and boundingPart and boundingPart.CFrame then
						table.insert(positions, {Model = model, CFrame = boundingPart.CFrame * fallbackOffset})
					end
				end
			end
		end
	end

	local result
	pcall(function()
		print(positions)
		result = ValidateMirror:InvokeServer(positions, currentFace)
	end)

Server calculations

if mirror and minBound and maxBound and face then
			local pivot = info.CFrame
			local pos = pivot.Position
			local mirroredPos

			if face == Enum.NormalId.Right then
				mirroredPos = Vector3.new(maxBound.X + (maxBound.X - pos.X), pos.Y, pos.Z)
			elseif face == Enum.NormalId.Left then
				mirroredPos = Vector3.new(minBound.X - (pos.X - minBound.X), pos.Y, pos.Z)
			elseif face == Enum.NormalId.Top then
				mirroredPos = Vector3.new(pos.X, maxBound.Y + (maxBound.Y - pos.Y), pos.Z)
			elseif face == Enum.NormalId.Bottom then
				mirroredPos = Vector3.new(pos.X, minBound.Y - (pos.Y - minBound.Y), pos.Z)
			elseif face == Enum.NormalId.Front then
				mirroredPos = Vector3.new(pos.X, pos.Y, minBound.Z - (pos.Z - minBound.Z))
			elseif face == Enum.NormalId.Back then
				mirroredPos = Vector3.new(pos.X, pos.Y, maxBound.Z + (maxBound.Z - pos.Z))
			end

			if mirroredPos then
				targetCFrame = CFrame.new(mirroredPos) * (pivot - pivot.Position)
			end
		end