How do you make a perfect snapping-system?

Hey programmers and developers,

I’ve been working on a part placing system for my building game. The core features are mostly working, but I keep running into imperfections with grid snapping and surface alignment.

When I try to snap parts to a grid or align them to surfaces:

  • Sometimes there’s a small gap or overlap.
  • It only works properly with very specific stud sizes (like multiples of the part size).
  • When rotating parts before placement, the alignment can shift, leaving hollow gaps of about 1 stud (or some oddly specific fraction).

This makes the placement feel inconsistent and “off” compared to how Roblox Studio itself handles alignment.

What I’m Looking For

  • Is there a better math/logic approach to make placement truly “flush” with surfaces?
  • How do I properly calculate the pivot so that rotations + grid snapping don’t create these awkward gaps?
  • Is there a reliable way to match surface normals and part extents so everything fits together seamlessly?

My Current Script

Here’s the part of my current implementation:

local function spawnPreview(materialName)
	Gui.EditFrame.AddFrame.MaterialsFrame.Visible = false
	clearPreview()
	Gui.EditFrame.ez.Visible = true

	local newPart = ReplicatedStorage:WaitForChild("Materials"):FindFirstChild(materialName):Clone()
	newPart.Anchored = true
	newPart.CanCollide = false
	newPart.Transparency = 0.5
	newPart.Parent = workspace.Camera

	previewPart = newPart

	if not isMobile then
		placingConnection = RunService.RenderStepped:Connect(updatePreviewPC)
		inputConnection = UserInputService.InputBegan:Connect(function(input, gameProcessed)
			if gameProcessed or not previewPart then return end
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				placeBlock()
			elseif input.KeyCode == Enum.KeyCode.T then
				rotX += 90
				updatePreviewPC()
			elseif input.KeyCode == Enum.KeyCode.R then
				rotY += 90
				updatePreviewPC()
			elseif input.KeyCode == Enum.KeyCode.Y then
				rotZ += 90
				updatePreviewPC()
			end
		end)
	else
		local mobileUI = Gui.EditFrame:WaitForChild("IAmMobileWoohoo")
		mobileUI.Visible = true

		local MoveMe = mobileUI:WaitForChild("MoveMe")
		MoveMe.Visible = true
		mobileUI.Tip.Visible = true

		local X = mobileUI:WaitForChild("X")
		local Y = mobileUI:WaitForChild("Y")
		local Z = mobileUI:WaitForChild("Z")
		X.Visible = true
		Y.Visible = true
		Z.Visible = true

		X.Activated:Connect(function() rotX += 90 updatePreviewMobile() end)
		Y.Activated:Connect(function() rotY += 90 updatePreviewMobile() end)
		Z.Activated:Connect(function() rotZ += 90 updatePreviewMobile() end)

		placingConnection = RunService.RenderStepped:Connect(updatePreviewMobile)

		local AcceptButton = Gui.EditFrame.AddFrame:WaitForChild("Accept")
		AcceptButton.Visible = true
		if AcceptButtonConnection then AcceptButtonConnection:Disconnect() end
		AcceptButtonConnection = AcceptButton.Activated:Connect(placeBlock)
	end
end

local function Build()
	local materials = Gui:WaitForChild("EditFrame")
		:WaitForChild("AddFrame")
		:WaitForChild("MaterialsFrame")
		:WaitForChild("Materials")

	local function startRotation(viewportFrame)
		local currentRotation = CFrame.new()
		task.spawn(function()
			while viewportFrame do
				currentRotation = currentRotation * CFrame.Angles(0, math.rad(2.5), 0)
				local part
				for _, child in ipairs(viewportFrame:GetChildren()) do
					if child:IsA("BasePart") then
						part = child
					end
				end
				part.CFrame = CFrame.new(part.Position) * currentRotation
				task.wait(0.02)
			end
		end)
	end

	for _, Material in ipairs(materials:GetChildren()) do
		if Material:IsA("ViewportFrame") then
			Material.TheButton.Activated:Connect(function()
				spawnPreview(Material.Name)
			end)
			startRotation(Material)
		end
	end
end

My Question to the Community

How do you make part placement systems perfectly snap to grid and surfaces
(like Roblox Studio does), even with rotation?

Any advice, examples, or resources would be greatly appreciated

2 Likes

Please avoid shenanigans like that:

Summary

Avoid using ipairs/pairs and use generic iteration instead;

For aligning i recomend to rely on normals (returned upon raycasting).

Also avoid indexing instances as much as possible (same with tables) and cache as much as you can see into variables.

Avoid creating closure functions like you did in the example i attached and avoid “thread spamming” if your function does not yield.


I dont really know why but I tried every thing to snap things perfectly aligned, when I used normals, its always that it ofc doesnt aligns properly, it also gave me an illusion everytime I tried to place☹️

Thanks for the help anyway

Hey there! So from my understanding, you want to emulate studio in Roblox with building features including grid snapping, snap to objects, and align with faces of snapped objects. Give me a minute to think for the latter 2, but for the first one (grid snapping positions), you can simply use the following function:

local function GridSnap(Value: number | Vector3, Size: number | Vector3)
	if typeof(Value) == "Vector3" then
		if typeof(Size) == "number" then
			return Vector3.new(
				math.floor(Value.X / Size + 0.5) * Size,
				math.floor(Value.Y / Size + 0.5) * Size,
				math.floor(Value.Z / Size + 0.5) * Size
			)
		elseif typeof(Size) == "Vector3" then
			return Vector3.new(
				math.floor(Value.X / Size.X + 0.5) * Size.X,
				math.floor(Value.Y / Size.Y + 0.5) * Size.Y,
				math.floor(Value.Z / Size.Z + 0.5) * Size.Z
			)
		else
			error("Size must be a number or Vector3")
		end
	elseif typeof(Value) == "number" then
		if typeof(Size) ~= "number" then
			error("If Value is a number, Size must also be a number")
		end
		return math.floor(Value / Size + 0.5) * Size
	else
		error("Value must be a number or Vector3")
	end
end

Edit: It’s getting late so I’ll have to come back tomorrow for the other 2 problems. Sorry :(

what kind of abomination is this lol?

1 Like

Take a look at EgoMoose’s implementation of a furniture placement it might be of great help:

I’m quite curious, aren’t closures in lua meant to be quite optimised anyways? Would the closure in this instance significantly affect the performance?

No.
Its a GC bloat in this example.
Closures are only benefitial when you store AND USE unique upvalues.

1 Like

Get connector parts on the objects that can be snapped together, loop through all of the connector parts and constantly check the distance between the placeholder part and the connection, if the distance is less than x amount, then the plceholder will snap to the connector position until it gets moved away from the range.

its a function i made for grid snapping…