How would I snap a part to another part in my building system?

Hello all,

Recently I’ve been developing my building script, and I’ve been trying to make it so whenever I move a model or a Basepart near another part, I want them to snap together seamlessly

Here is a video showing my problem:

You can see that it doesnt snap but rather freely move around even when it nears to the other wall
Here’s my script, this function is ran every frame:

local function MoveCurrentObjectToMouse(dt)
	if not CurrentObject then
		return
	end
	if PerformanceMode then -- performance mode
		mcotm_elapsed += dt
		if mcotm_elapsed < mcotm_interval then
			return
		end
		mcotm_elapsed = 0
	end

	Mouse.TargetFilter = CurrentObject

	local hitPos = Mouse.Hit.Position
	local ray_origin = hitPos + Vector3.new(0, 10, 0)
	local ray_direction = Vector3.new(0, -30, 0)

     -- limit distance of placing an object
	if CurrentObject:IsA("Model") then
		if (RootPart.Position - hitPos).Magnitude > 70 then
			return
		end
	elseif CurrentObject:IsA("BasePart") then
		if (RootPart.Position - hitPos).Magnitude > 70 then
			return
		end
	else
		warn("Object class is not configured for placing yet")
		return
	end
	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {workspace.Terrain}
	rayParams.FilterType = Enum.RaycastFilterType.Include

	local rayResult = workspace:Raycast(ray_origin, ray_direction, rayParams)
	if not rayResult then -- if there's no ground beneath
		return
	end

	if CurrentObject:IsA("Model") then
		CurrentObject:MoveTo(hitPos + Vector3.new(0, CurrentObject:GetExtentsSize().Y/2, 0))
	else
		CurrentObject.Position = hitPos + Vector3.new(0, CurrentObject.Size.Y/2, 0)
	end
end

Help would be greatly appreciated!

1 Like

A good start would be to list out exactly how you want the snapping to behave - like should it be based on the surface you’re targeting? Or maybe if you position it near one of the six “perfect” positions it should snap into place? Some combination of both?

Once you’ve defined exactly how you want it to work, your if statement in this function basically ends up looking exactly the same as your description.

1 Like

how I would want it to behave is when one of the object’s faces being placed gets close to one of the faces of the already placed object, it snaps and the two faces make flush.

e.g. I have a wall here

I move a wall I’m going to place close to it

It snaps and aligns itself perfectly, continuing the wall seamlessly

The issue could be that your current code only positions objects at the mouse location without checking for nearby parts to snap to.

Here’s an improved version with snapping logic:

local SNAP_DISTANCE = 2
local SNAP_ENABLED = true

local function GetObjectSize(object)
	if object:IsA("Model") then
		return object:GetExtentsSize()
	else
		return object.Size
	end
end

local function GetObjectPosition(object)
	if object:IsA("Model") then
		return object:GetPivot().Position
	else
		return object.Position
	end
end

local function SetObjectPosition(object, position)
	if object:IsA("Model") then
		object:PivotTo(CFrame.new(position))
	else
		object.Position = position
	end
end

local function FindNearbyParts(position, searchRadius)
	local parts = {}
	local region = Region3.new(
		position - Vector3.new(searchRadius, searchRadius, searchRadius),
		position + Vector3.new(searchRadius, searchRadius, searchRadius)
	)
	region = region:ExpandToGrid(4)
	
	local overlapParams = OverlapParams.new()
	overlapParams.FilterDescendantsInstances = {CurrentObject}
	overlapParams.FilterType = Enum.RaycastFilterType.Exclude
	
	parts = workspace:GetPartBoundsInBox(CFrame.new(position), Vector3.new(searchRadius * 2, searchRadius * 2, searchRadius * 2), overlapParams)
	
	return parts
end

local function CalculateSnapPosition(currentPos, currentSize)
	if not SNAP_ENABLED then
		return currentPos
	end
	
	local nearbyParts = FindNearbyParts(currentPos, SNAP_DISTANCE + 10)
	
	local bestSnapPos = currentPos
	local closestDist = math.huge
	
	for _, part in ipairs(nearbyParts) do
		if part.Parent == workspace.Terrain or part == CurrentObject then
			continue
		end
		
		local partPos = part.Position
		local partSize = part.Size
		
		local snapPoints = {
			Vector3.new(partPos.X + partSize.X/2 + currentSize.X/2, currentPos.Y, partPos.Z),
			Vector3.new(partPos.X - partSize.X/2 - currentSize.X/2, currentPos.Y, partPos.Z),
			Vector3.new(partPos.X, currentPos.Y, partPos.Z + partSize.Z/2 + currentSize.Z/2),
			Vector3.new(partPos.X, currentPos.Y, partPos.Z - partSize.Z/2 - currentSize.Z/2),
			Vector3.new(partPos.X, partPos.Y + partSize.Y/2 + currentSize.Y/2, partPos.Z),
			Vector3.new(partPos.X, partPos.Y - partSize.Y/2 - currentSize.Y/2, partPos.Z),
		}
		
		for _, snapPoint in ipairs(snapPoints) do
			local dist = (currentPos - snapPoint).Magnitude
			
			if dist < SNAP_DISTANCE and dist < closestDist then
				closestDist = dist
				bestSnapPos = snapPoint
			end
		end
	end
	
	return bestSnapPos
end

local function MoveCurrentObjectToMouse(dt)
	if not CurrentObject then
		return
	end
	if PerformanceMode then
		mcotm_elapsed += dt
		if mcotm_elapsed < mcotm_interval then
			return
		end
		mcotm_elapsed = 0
	end

	Mouse.TargetFilter = CurrentObject

	local hitPos = Mouse.Hit.Position
	local ray_origin = hitPos + Vector3.new(0, 10, 0)
	local ray_direction = Vector3.new(0, -30, 0)

	local objectSize = GetObjectSize(CurrentObject)
	if (RootPart.Position - hitPos).Magnitude > 70 then
		return
	end

	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {workspace.Terrain}
	rayParams.FilterType = Enum.RaycastFilterType.Include

	local rayResult = workspace:Raycast(ray_origin, ray_direction, rayParams)
	if not rayResult then
		return
	end

	local basePos = hitPos + Vector3.new(0, objectSize.Y/2, 0)
	
	local snappedPos = CalculateSnapPosition(basePos, objectSize)
	
	SetObjectPosition(CurrentObject, snappedPos)
end

local function CreateSnapIndicator()
	local indicator = Instance.new("Part")
	indicator.Size = Vector3.new(0.5, 0.5, 0.5)
	indicator.Anchored = true
	indicator.CanCollide = false
	indicator.Material = Enum.Material.Neon
	indicator.Color = Color3.fromRGB(0, 255, 0)
	indicator.Transparency = 0.5
	indicator.Name = "SnapIndicator"
	indicator.Parent = workspace
	return indicator
end

The CreateSnapIndicator function is just for visual feedback and I’m not 100% if this works

you could get the mouse target, and a normal of that target (assuming it’s a placed object), then simply pivot the blueprint on that normal face of the mouse target (y axis remains on the floor by raycast or wtv), then you could have a flag like snappingToBuilding and make it rotate relatively to the mouse target (like a wall path)

1 Like