Placement positioning is wrong

I can’t seem to calculate the position correctly.

Code:

local RunService = game:GetService("RunService")

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

local tool = script.Parent
local SelectionPart = script:WaitForChild("SelectionPart")
local SelectionBox = script:WaitForChild("SelectionBox")

local gridSize = 3

local connection = nil

local surfaceData = {
	["Front"] = Vector3.new(0, 0, 1),
	["Back"] = Vector3.new(0, 0, -1),

	["Top"] = Vector3.new(0, 1, 0),
	["Bottom"] = Vector3.new(0, -1, 0),

	["Left"] = Vector3.new(-1, 0, 0),
	["Right"] = Vector3.new(1, 0, 0),
}

local function Unequip()
	if connection then
		connection:Disconnect()
		connection = nil
	end

	SelectionBox.Visible = false
end

local function Equip()
	SelectionBox.Visible = true

	connection = RunService.Heartbeat:Connect(function()
		local mouseTarget = mouse.Target
		local mousePos = mouse.Hit.Position

		if mouseTarget then
			local targetSurface = mouse.TargetSurface
			local surface = surfaceData[targetSurface.Name]

			if surface then
				local targetPos = mouseTarget.Position
				local targetSize = mouseTarget.Size
				local offset = targetPos + (targetSize / 2 * surface) + (Vector3.one * gridSize / 2 * surface)
				
				local position = mousePos + offset

				SelectionPart.Position = position
			end
		end
	end)
end

tool.Equipped:Connect(function()
	Equip()
end)

tool.Unequipped:Connect(function()
	Unequip()
end)

Are you looking to create a free form placement system, or a grid-based one?

Just a free form placement system.

I tried a different approach but the offset is off:

local RunService = game:GetService("RunService")

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

local tool = script.Parent
local SelectionPart = script:WaitForChild("SelectionPart")
local SelectionBox = script:WaitForChild("SelectionBox")

local gridSize = 3

local connection = nil

local surfaceData = {
	["Front"] = Vector3.new(0, 0, -1),
	["Back"] = Vector3.new(0, 0, 1),

	["Top"] = Vector3.new(0, 1, 0),
	["Bottom"] = Vector3.new(0, -1, 0),

	["Left"] = Vector3.new(-1, 0, 0),
	["Right"] = Vector3.new(1, 0, 0),
}

local function Unequip()
	if connection then
		connection:Disconnect()
		connection = nil
	end

	SelectionBox.Visible = false
end

local function SnapToGrid(x)
	return math.floor(x / gridSize) * gridSize
end

local function Equip()
	SelectionBox.Visible = true

	connection = RunService.Heartbeat:Connect(function()
		local mouseTarget = mouse.Target
		local mousePos = mouse.Hit.Position

		if mouseTarget then
			local targetSurface = mouse.TargetSurface
			local surface = surfaceData[targetSurface.Name]

			if surface then
				local targetPos = mouseTarget.Position
				local targetSize = mouseTarget.Size
				local position = mousePos + (Vector3.one * gridSize / 2 * surface)
				position = Vector3.new(SnapToGrid(position.X), SnapToGrid(position.Y), SnapToGrid(position.Z))

				SelectionPart.Position = position
			end
		end
	end)
end

tool.Equipped:Connect(function()
	Equip()
end)

tool.Unequipped:Connect(function()
	Unequip()
end)

It works perfectly without the grid but adding the grid ruins the positioning

I fixed it. My map was just made wrong.

You can make a targetPosition that offsets the mouse.Hit.Position by the Y value divided by 2 of the part, then set the part’s position to the targetPosition.

In the heartbeat event:

local targetPos = mouse.Hit.Position + Vector3.new(0, SelectionPart.Size.Y/2, 0)

SelectionPart.Position = targetPos

You could also put the part into a model and use Model:MoveTo() which automatically avoids collisions, but it always resets the orientation of the model.