Placement system isn't snapping

Been trying to make it so that it snaps to the edge of a block depending on where you’re pointing, also relative to size.
Works fine for full size, but any other size that isn’t the same goes to the VERY edge where half of it is leaning off of it, or it goes to the centre.

local mouse = game.Players.LocalPlayer:GetMouse()
local connection

game:GetService("RunService").Heartbeat:Connect(function()
	local sizing = script.Parent.Properties.Size.Value

	if script.Parent.Properties.DoNotApply.Mode.Value == 0 then
		if connection then
			connection:Disconnect()
		end

		if workspace.Client:FindFirstChild("PreviewBlock") then
			workspace.Client:FindFirstChild("PreviewBlock"):Destroy()
		end

		if mouse.Target then
			local hitpos = mouse.Hit.Position
			local part = game.ReplicatedStorage.Files.BlockTypes:FindFirstChild(script.Parent.Properties.Shape.Value):Clone()
			part.Size = sizing
			part.Anchored = true

			local gridpos = Vector3.new(
				math.floor((hitpos.X + sizing.X / 2) / sizing.X) * sizing.X,
				math.floor((hitpos.Y + sizing.Y / 2) / sizing.Y) * sizing.Y,
				math.floor((hitpos.Z + sizing.Z / 2) / sizing.Z) * sizing.Z
			)

			local alignpos = gridpos + Vector3.new(0, sizing.Y / 2, 0)

			part.CFrame = CFrame.new(alignpos)
			part.Parent = workspace.Client
			part.Name = "PreviewBlock"
			connection = mouse.Button1Down:Connect(function()
				local newblock = part:Clone()
				newblock.Anchored = false
				newblock.Parent = workspace
			end)
		end
	elseif script.Parent.Properties.DoNotApply.Mode.Value == 1 then
	end
end)

For reference, it’s meant to snap like this for example:
Screenshot 2025-01-01 151715

But, it snaps like this:

Or this:
Screenshot 2025-01-01 154606

Any help’s appreciated, project is rather big.

You should try raycasting with WorldRoot:Raycast(). It gives the exact point and normal of where the raycast (mouse)

local result = workspace:Raycast(
	workspace.CurrentCamera.CFrame.Position, -- origin
	(player:GetMouse().Hit.Position - workspace.CurrentCamera.CFrame.Position).Unit * 1000, -- direction
	RaycastParams.new())

print(result.Position) -- the position it found (ignores bounding boxes)
print(result.Normal) -- the direction the surface is facing (useful for placing objects flat on others)


This is what it results when I’ve implemented it. It still seems to be offset.

game:GetService("RunService").Heartbeat:Connect(function()
	local sizing = script.Parent.Properties.Size.Value

	if script.Parent.Properties.DoNotApply.Mode.Value == 0 then
		if connection then
			connection:Disconnect()
		end

		if workspace.Client:FindFirstChild("PreviewBlock") then
			workspace.Client:FindFirstChild("PreviewBlock"):Destroy()
		end

		if mouse.Target then
			local hitpos = mouse.Hit.Position
			local part = game.ReplicatedStorage.Files.BlockTypes:FindFirstChild(script.Parent.Properties.Shape.Value):Clone()
			part.Size = sizing
			part.Anchored = true
			part.CanCollide = false
			part.Color = script.Parent.Properties.Color.Value
			part.Material = Enum.Material[script.Parent.Properties.Material.Value]
			part.Transparency = script.Parent.Properties.Transparency.Value
			part.Reflectance = script.Parent.Properties.Reflectance.Value
			local result = workspace:Raycast(
				workspace.CurrentCamera.CFrame.Position, -- origin
				(game.Players.LocalPlayer:GetMouse().Hit.Position - workspace.CurrentCamera.CFrame.Position).Unit * 1000, -- direction
				RaycastParams.new()
			)

			local finalpos
			if result then
				finalpos = result.Position
			else
				finalpos = (workspace.CurrentCamera.CFrame.Position + (game.Players.LocalPlayer:GetMouse().Hit.Position - workspace.CurrentCamera.CFrame.Position).Unit * 1000)
			end

			local gridSize = getgrid(sizing)
			finalpos = Vector3.new(
				math.floor(finalpos.X / gridSize) * gridSize,
				math.floor(finalpos.Y / gridSize) * gridSize,
				math.floor(finalpos.Z / gridSize) * gridSize
			)

			part.CFrame = CFrame.new(finalpos + Vector3.new(0, sizing.Y / 2, 0))
			part.Parent = workspace.Client
			part.Name = "PreviewBlock"
			connection = mouse.Button1Down:Connect(function()
				local newblock = part:Clone()
				newblock.CanCollide = script.Parent.Properties.Collision.Value
				newblock.Anchored = game.ReplicatedStorage.Files.BlockTypes:FindFirstChild(script.Parent.Properties.Shape.Value).Anchored
				newblock.Parent = workspace
			end)
		end
	elseif script.Parent.Properties.DoNotApply.Mode.Value == 1 then
	end
end)

function getgrid(size)
	local sizes = {size.X, size.Y, size.Z}
	local counts = {}
	for _, s in ipairs(sizes) do
		counts[s] = (counts[s] or 0) + 1
	end
	local maxCount = 0
	local commonSize = sizes[1]
	for size, count in pairs(counts) do
		if count > maxCount then
			maxCount = count
			commonSize = size
		end
	end
	return commonSize
end
1 Like

I’m struggling to understand what you want exactly, can you send an example? Is this not just a grid system?

Well it is the grid system, but…

For some reason, the centre of the part seems to snap to the edge. I’m attempting to aim for an effect where its connected to the edge, like this:
Screenshot 2025-01-01 151715

Bumped, because I got half of it working.

local mouse = game.Players.LocalPlayer:GetMouse()
local connection
local connection2
local highlight = Instance.new("Highlight")
game:GetService("RunService").Heartbeat:Connect(function()
	local sizing = script.Parent.Properties.Size.Value
	local gridX, gridY, gridZ = sizing.X, sizing.Y, sizing.Z

	if script.Parent.Properties.DoNotApply.Mode.Value == 0 then
		if connection then
			connection:Disconnect()
		end
		if connection2 then
			connection2:Disconnect()
		end
		if workspace.Client:FindFirstChild("PreviewBlock") then
			workspace.Client:FindFirstChild("PreviewBlock"):Destroy()
		end

		if mouse.Target then
			local hitpos = mouse.Hit.Position
			local part = game.ReplicatedStorage.Files.BlockTypes:FindFirstChild(script.Parent.Properties.Shape.Value):Clone()
			part.Size = sizing
			part.Anchored = true
			part.CanCollide = false
			part.Color = script.Parent.Properties.Color.Value
			part.Material = Enum.Material[script.Parent.Properties.Material.Value]
			part.Transparency = script.Parent.Properties.Transparency.Value
			part.Reflectance = script.Parent.Properties.Reflectance.Value

			local raycastParams = RaycastParams.new()
			raycastParams.FilterDescendantsInstances = {game.Players.LocalPlayer.Character, part}
			raycastParams.FilterType = Enum.RaycastFilterType.Exclude
			local result = workspace:Raycast(
				workspace.CurrentCamera.CFrame.Position,
				(mouse.Hit.Position - workspace.CurrentCamera.CFrame.Position).Unit * 1000,
				raycastParams
			)

			local finalPos
			if result then
				local hitBlock = result.Instance
				local blockPos = hitBlock.Position
				local blockSize = hitBlock.Size
				local surfaceNormal = result.Normal
				local hitPos = result.Position

				local localHitX = hitPos.X - (blockPos.X - blockSize.X / 2)
				local localHitZ = hitPos.Z - (blockPos.Z - blockSize.Z / 2)
				local localHitY = hitPos.Y - (blockPos.Y - blockSize.Y / 2)

				local snappedLocalX = math.floor(localHitX / gridX) * gridX
				local snappedLocalZ = math.floor(localHitZ / gridZ) * gridZ
				local snappedLocalY = math.floor(localHitY / gridY) * gridY

				finalPos = Vector3.new(
					blockPos.X - blockSize.X / 2 + snappedLocalX + gridX / 2,
					blockPos.Y - blockSize.Y / 2 + snappedLocalY + gridY / 2,
					blockPos.Z - blockSize.Z / 2 + snappedLocalZ + gridZ / 2
				)

				if math.abs(surfaceNormal.X) > math.abs(surfaceNormal.Y) and math.abs(surfaceNormal.X) > math.abs(surfaceNormal.Z) then
					finalPos = Vector3.new(
						math.clamp(finalPos.X, blockPos.X - blockSize.X / 2, blockPos.X + blockSize.X / 2),
						finalPos.Y,
						finalPos.Z
					)
				elseif math.abs(surfaceNormal.Y) > math.abs(surfaceNormal.X) and math.abs(surfaceNormal.Y) > math.abs(surfaceNormal.Z) then
					finalPos = Vector3.new(
						finalPos.X,
						math.clamp(finalPos.Y, blockPos.Y - blockSize.Y / 2, blockPos.Y + blockSize.Y / 2),
						finalPos.Z
					)
				else
					finalPos = Vector3.new(
						finalPos.X,
						finalPos.Y,
						math.clamp(finalPos.Z, blockPos.Z - blockSize.Z / 2, blockPos.Z + blockSize.Z / 2)
					)
				end
			else
				finalPos = Vector3.new(
					math.floor(hitpos.X / gridX) * gridX,
					math.floor(hitpos.Y / gridY) * gridY,
					math.floor(hitpos.Z / gridZ) * gridZ
				)
			end
			part.CFrame = CFrame.new(finalPos) + Vector3.new(0, sizing.Y / 2, 0)
			part.Parent = workspace.Client
			part.Name = "PreviewBlock"
			part.Orientation = script.Parent.Properties.Orientation.Value

			connection = mouse.Button1Down:Connect(function()
				local tables = {
					Transparency = script.Parent.Properties.Transparency.Value,
					Reflectance = script.Parent.Properties.Reflectance.Value,
					Collision = script.Parent.Properties.Collision.Value,
					LightRadius = script.Parent.Properties.Light.Radius.Value,
					LightType = script.Parent.Properties.Light.Type.Value,
					LightBrightness = script.Parent.Properties.Light.Brightness.Value,
					LightColor = script.Parent.Properties.Light.Color.Value,
					Shape = script.Parent.Properties.Shape.Value,
					Color = script.Parent.Properties.Color.Value,
					Material = script.Parent.Properties.Material.Value,
					Size = script.Parent.Properties.Size.Value,
					Position = part.CFrame
				}
				local event = game.ReplicatedStorage.Files.Events.NonNative.Place
				event:FireServer(tables)
			end)
		end
	elseif script.Parent.Properties.DoNotApply.Mode.Value == 1 then
		if mouse.Target and mouse.Target.Parent == game.Workspace.Blocks then
			local blockpos = mouse.Hit.Position
			highlight.Parent = mouse.Target
			if connection then
				connection:Disconnect()
			end
			if connection2 then
				connection2:Disconnect()
			end

			connection2 = mouse.Button1Down:Connect(function()
				local target = mouse.Target
				if target and target.Parent == game.Workspace.Blocks then
					local event = game.ReplicatedStorage.Files.Events.NonNative.Delete
					event:FireServer(target)
					highlight.Parent = nil
				end
			end)
		else
			if highlight.Parent then
				highlight.Parent = nil
			end
			if connection2 then
				connection2:Disconnect()
				connection2 = nil
			end
		end
	end
end)

It works at the top but is finnicky on all the other sides. Any fixes?

Fixed part of it myself because NOT A SINGLE PERSON replied.
Wow.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.