The getPlacementY() function only works for placing on top because it only thinks about the Y axis. For placing below or on the sides, you need to know which face of the part the player is pointing at
That is possible. If you’re using a raycast RaycastResult.Normal tells you the direction of the surface that was hit
For example
-- top face
Vector3.new(0, 1, 0)
-- bottom face
Vector3.new(0, -1, 0)
-- right / left faces
Vector3.new(1, 0, 0)
Vector3.new(-1, 0, 0)
-- front / back faces
Vector3.new(0, 0, 1)
Vector3.new(0, 0, -1)
So instead of only calculating the Y offset, you can offset the preview in the direction of the surface normal
Something like this for axis-aligned blocks
local gridSize = 1.25
local function snapToGrid(value)
return math.round(value / gridSize) * gridSize
end
local function getMainAxis(normal)
local absX = math.abs(normal.X)
local absY = math.abs(normal.Y)
local absZ = math.abs(normal.Z)
if absX > absY and absX > absZ then
return Vector3.new(math.sign(normal.X), 0, 0)
elseif absY > absX and absY > absZ then
return Vector3.new(0, math.sign(normal.Y), 0)
else
return Vector3.new(0, 0, math.sign(normal.Z))
end
end
local function getHalfSizeInDirection(part, normal)
return (
math.abs(normal.X) * part.Size.X / 2 +
math.abs(normal.Y) * part.Size.Y / 2 +
math.abs(normal.Z) * part.Size.Z / 2
)
end
local function getPlacementPosition(raycastResult, placingPart)
local normal = getMainAxis(raycastResult.Normal)
local hitPosition = raycastResult.Position
local placingHalfSize = getHalfSizeInDirection(placingPart, normal)
local x = snapToGrid(hitPosition.X)
local y = snapToGrid(hitPosition.Y)
local z = snapToGrid(hitPosition.Z)
if normal.X ~= 0 then
x = hitPosition.X + normal.X * placingHalfSize
elseif normal.Y ~= 0 then
y = hitPosition.Y + normal.Y * placingHalfSize
elseif normal.Z ~= 0 then
z = hitPosition.Z + normal.Z * placingHalfSize
end
return Vector3.new(x, y, z)
end
Then after raycasting
local result = workspace:Raycast(origin, direction, raycastParams)
if result and result.Instance then
local hit = result.Instance
local config = hit:FindFirstChild("BuildConfig")
local buildType = config and config:GetAttribute("BuildPartType")
if buildType == "Floor" or buildType == "Block" then
previewPart.Position = getPlacementPosition(result, previewPart)
end
end
The important part is this
local normal = result.Normal
That normal is what tells you what side of the part was selected
So if the player points at the top of a block, the normal is roughly
Vector3.new(0, 1, 0)
If they point at the bottom, it is
Vector3.new(0, -1, 0)
If they point at the side, it will be one of the X or Z directions
This also removes the need for a fixed .375 offset because the preview is moved by half of its own size in the direction of the face that was clicked
For example, when placing on the right side of a block, it does
x = hitPosition.X + placingPart.Size.X / 2
When placing on top, it does
y = hitPosition.Y + placingPart.Size.Y / 2
When placing below, it does
y = hitPosition.Y - placingPart.Size.Y / 2
So the same logic works for top, bottom and sides
One thing to keep in mind though. This version assumes your blocks arent rotated. If your blocks can be rotated, then you would need to do the same idea in the part’s object space using CFrame:VectorToObjectSpace() / CFrame:PointToObjectSpace(). But for a normal grid building system with straight blocks RaycastResult.Normal should be enough.