Recently I was looking around to see if I could find a faster way to furnish the buildings I was making, and I found a placement system that works great for it, but I want to be able to tilt the objects I'm placing, because plain rotation isn't the best.

The system has 3 scripts, and I added 1 as well.

Server Script :

local datastore = game:GetService("DataStoreService"):GetDataStore("PlacementSystem")

local placementClass = require(game:GetService("ReplicatedStorage"):WaitForChild("Placement"))
local placementObjects = {}

local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes")

--

function remotes.InitPlacement.OnServerInvoke(player, canvasPart)
	placementObjects[player] = placementClass.new(canvasPart)
	return placementObjects[player].CanvasObjects
end

remotes.InvokePlacement.OnServerEvent:Connect(function(player, func, ...)
	if (placementObjects[player]) then
		placementClass[func](placementObjects[player], ...)
	end
end)

function remotes.DSPlacement.OnServerInvoke(player, saving, useData)
	local key = "player_"..player.UserId
	
	local success, result = pcall(function()
		if (saving and placementObjects[player]) then
			if (useData) then
				datastore:SetAsync(key, placementObjects[player]:Serialize())
			else
				datastore:SetAsync(key, {})
			end
		elseif (not saving) then
			return datastore:GetAsync(key)
		end
	end)
	
	if (success) then
		return saving or result
	else
		warn(result)
	end
end

Module script:

local isServer = game:GetService("RunService"):IsServer()

local furniture = game:GetService("ReplicatedStorage"):WaitForChild("Furniture")

local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes")
local initPlacement = remotes:WaitForChild("InitPlacement")
local invokePlacement = remotes:WaitForChild("InvokePlacement")
local dsPlacement = remotes:WaitForChild("DSPlacement")

--

local Placement = {}
Placement.__index = Placement

-- constructor

function Placement.new(canvasPart)
	local self = setmetatable({}, Placement)

	self.CanvasPart = canvasPart
	
	if (isServer) then
		self.CanvasObjects = Instance.new("Folder")
		self.CanvasObjects.Name = "CanvasObjects"
		self.CanvasObjects.Parent = canvasPart
	else
		self.CanvasObjects = initPlacement:InvokeServer(canvasPart)
	end
	
	self.Surface = Enum.NormalId.Top
	self.GridUnit = 1

	return self
end

function Placement.fromSerialization(canvasPart, data)
	local self = Placement.new(canvasPart)
	local canvasCF = canvasPart.CFrame
	data = data or {}
	
	for cf, name in pairs(data) do
		local model = furniture:FindFirstChild(name)
		if (model) then
			local components = {}
			for num in string.gmatch(cf, "[^%s,]+") do
				components[#components+1] = tonumber(num)
			end
			
			self:Place(model, canvasCF * CFrame.new(unpack(components)), false)
		end
	end
	
	return self
end

-- methods

function Placement:CalcCanvas()
	local canvasSize = self.CanvasPart.Size

	local up = Vector3.new(0, 1, 0)
	local back = -Vector3.FromNormalId(self.Surface)
	
	local dot = back:Dot(Vector3.new(0, 1, 0))
	local axis = (math.abs(dot) == 1) and Vector3.new(-dot, 0, 0) or up
	
	local right = CFrame.fromAxisAngle(axis, math.pi/2) * back
	local top = back:Cross(right).unit

	local cf = self.CanvasPart.CFrame * CFrame.fromMatrix(-back*canvasSize/2, right, top, back)
	--local size = Vector2.new((canvasSize * right).magnitude, (canvasSize * top).magnitude)
	local size = Vector2.new(100000, 100000)-- gives near infinite range
	return cf, size
end

function Placement:CalcPlacementCFrame(model, position, rotation)-- rotation
	local cf, size = self:CalcCanvas()

	local modelSize = CFrame.fromEulerAnglesYXZ(0, rotation, 0) * model.PrimaryPart.Size
	modelSize = Vector3.new(math.abs(modelSize.x), math.abs(modelSize.y), math.abs(modelSize.z))

	local lpos = cf:pointToObjectSpace(position);
	local size2 = (size - Vector2.new(modelSize.x, modelSize.z))/2
	local x = math.clamp(lpos.x, -size2.x, size2.x);
	local y = math.clamp(lpos.y, -size2.y, size2.y);
	--
	--local g = self.GridUnit
	--if (g > 0) then
	--	x = math.sign(x)*((math.abs(x) - math.abs(x) % g) + (size2.x % g))
	--	y = math.sign(y)*((math.abs(y) - math.abs(y) % g) + (size2.y % g))
	--end
	
	return cf * CFrame.new(x, y, -modelSize.y/2) * CFrame.Angles(-math.pi/2, rotation, 0)
end

function Placement:CalcPlacementCFrame2(model, position, rotation)-- atempt at tilt
	local cf, size = self:CalcCanvas()

	local modelSize = CFrame.fromEulerAnglesYXZ(0, rotation, 0) * model.PrimaryPart.Size
	modelSize = Vector3.new(math.abs(modelSize.x), math.abs(modelSize.y), math.abs(modelSize.z))

	local lpos = cf:pointToObjectSpace(position);
	local size2 = (size - Vector2.new(modelSize.x, modelSize.z))/2
	local x = math.clamp(lpos.x, -size2.x, size2.x);
	local y = math.clamp(lpos.y, -size2.y, size2.y);
	--
	--local g = self.GridUnit
	--if (g > 0) then
	--	x = math.sign(x)*((math.abs(x) - math.abs(x) % g) + (size2.x % g))
	--	y = math.sign(y)*((math.abs(y) - math.abs(y) % g) + (size2.y % g))
	--end
	
	return cf * CFrame.new(x, y, -modelSize.y/2) * CFrame.Angles(rotation, -math.pi/2, 0)
end

function Placement:isColliding(model)
	local isColliding = false

	local touch = model.PrimaryPart.Touched:Connect(function() end)
	local touching = model.PrimaryPart:GetTouchingParts()
	
	for i = 1, #touching do
		if (not touching[i]:IsDescendantOf(model)) then
			--isColliding = true
			break
		end
	end

	touch:Disconnect()
	return isColliding
end

function Placement:Place(model, cf, isColliding)
	if (not isColliding and isServer) then
		local clone = model:Clone()
		clone:SetPrimaryPartCFrame(cf)
		clone.Parent = self.CanvasObjects
	end

	if (not isServer) then
		invokePlacement:FireServer("Place", model, cf, isColliding)
	end
end

function Placement:Serialize()
	local serial = {}
	
	local cfi = self.CanvasPart.CFrame:inverse()
	local children = self.CanvasObjects:GetChildren()
	
	for i = 1, #children do
		serial[tostring(cfi * children[i].PrimaryPart.CFrame)] = children[i].Name 
	end
	
	return serial
end

function Placement:Save()
	local success = dsPlacement:InvokeServer(true, true)
	if (success) then
		print("Saved")
	end
end

function Placement:Clear()
	self.CanvasObjects:ClearAllChildren()
	
	if (not isServer) then
		invokePlacement:FireServer("Clear")
		local success = dsPlacement:InvokeServer(true, false)
		if (success) then
			print("Cleared")
		end
	end
end

--

return Placement

Client Script:

local canvas = game.Workspace:WaitForChild("ExampleCanvas")
local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes")
local furniture = game:GetService("ReplicatedStorage"):WaitForChild("Furniture")
local pieces = furniture:GetChildren()

local placementClass = require(game:GetService("ReplicatedStorage"):WaitForChild("Placement"))
local placement = placementClass.fromSerialization(canvas, remotes.DSPlacement:InvokeServer(false, false))
placement.GridUnit = 2

local mouse = game.Players.LocalPlayer:GetMouse()
mouse.TargetFilter = placement.CanvasObjects

local rotation = 0
local rotation2 = 0
local modelCount = 0
local model = nil

--

local function onTilt(actionName, userInputState, input)
	if (userInputState == Enum.UserInputState.Begin) then
		rotation2 = rotation2 + math.pi/15
	end
end

local function onSwitch(actionName, userInputState, input)
	if (userInputState == Enum.UserInputState.Begin) then
		if (model) then
			model:Destroy()
		end
		modelCount = modelCount + 1 > #pieces and 1 or modelCount + 1
		model = pieces[modelCount]:Clone()
		model.Parent = placement.CanvasObjects
	end
end

local function onRotate(actionName, userInputState, input)
	if (userInputState == Enum.UserInputState.Begin) then
		rotation = rotation + math.pi/15
	end
end

local function onPlace(actionName, userInputState, input)
	if (userInputState == Enum.UserInputState.Begin) then
		local cf = placement:CalcPlacementCFrame(model, mouse.Hit.p, rotation)
		placement:Place(furniture[model.Name], cf, placement:isColliding(model))
	end
end

local function onClear(actionName, userInputState, input)
	if (userInputState == Enum.UserInputState.Begin) then
		model.Parent = nil
		placement:Clear()
		model.Parent = mouse.TargetFilter
	end
end

local function onSave(actionName, userInputState, input)
	if (userInputState == Enum.UserInputState.Begin) then
		placement:Save()
	end
end

--

onSwitch(nil, Enum.UserInputState.Begin, nil)

game:GetService("ContextActionService"):BindAction("switch", onSwitch, false, Enum.KeyCode.E)
game:GetService("ContextActionService"):BindAction("rotate", onRotate, false, Enum.KeyCode.R)
game:GetService("ContextActionService"):BindAction("tilt", onTilt, false, Enum.KeyCode.T)-- new
game:GetService("ContextActionService"):BindAction("place", onPlace, false, Enum.UserInputType.MouseButton1)
game:GetService("ContextActionService"):BindAction("clear", onClear, false, Enum.KeyCode.C)
game:GetService("ContextActionService"):BindAction("save", onSave, false, Enum.KeyCode.F)

game:GetService("RunService").RenderStepped:Connect(function(dt)
	local cf = placement:CalcPlacementCFrame(model, mouse.Hit.p, rotation2 and rotation)
	model:SetPrimaryPartCFrame(cf)
end)

the script I added was only to adjust the Y level it was placing the furniture on to the Character's HumanoidRootPart Y level.

As a large portion of people probably noticed, there's a lot of stuff in there that kinda works, but for some reason I can only have rotation, or tilting, never both.

so in short, I'd like to know why binding the action doesn't work properly and how I can fix this issue.

Side note: this is not the original version, this is my highly edited version, the original version is here : (http://)https://www.google.com/url?q=https%3A%2F%2Fscriptinghelpers.org%2Fblog%2Fcreating-a-furniture-placement-system&sa=D&sntz=1&usg=AFQjCNHNt9BQ9GkLQZRcR0GzsVCxIhZdBg