Placement system issue

I am using @tunicus’s placement system API to make a basic placement system for a roblox game that I got hired to work on. I also made it so the client presses ‘R’ to rotate the model they are placing. However, when I place a rotated model, it does not rotate on the server.

Here is a video of it happening:

If you place the model before rotating it, then it works perfectly fine, but if you rotate the model then place it, for some reason the server won’t bother to rotate it either (Even though it should).

Here is the model which has all the required items:

https://www.roblox.com/library/4446410121/Placement-Help

Put the Gui in starter gui, put the script in server script service, lastly leave the model where it is (In the workspace obviously lmao).

Here is the client script:

local placementHandler = require(script.PlacementModule)

local base = workspace.Base

local items = game:GetService("ReplicatedStorage"):WaitForChild("Items")

local plane = placementHandler.new(base:WaitForChild("Baseplate"), base:WaitForChild("ItemHolder"), 1)

local dummyModel
local signal
local currenItemName
local orientation

local function cancelPlacement()
	if (currenItemName) then
		plane:disable()
		if (dummyModel.Parent) then
			dummyModel:Destroy()
		end
		dummyModel = nil
		signal = nil
		currenItemName = nil
	end
end

local function initiatePlacement(itemName)
	cancelPlacement()
	dummyModel = items[itemName]:clone()
	currenItemName = itemName
	dummyModel.Parent = workspace

	local signal = plane:enable(dummyModel)
	
	signal:connect(function(location, _)
		orientation = dummyModel.PrimaryPart.Orientation.Y
		game:GetService("ReplicatedStorage"):WaitForChild("ClientPlaced"):FireServer(currenItemName, location[1], orientation, base)
		--cancelPlacement()
	end)
	game:GetService("UserInputService").InputBegan:connect(function(inputObject, gameProcessedEvent)
		if not gameProcessedEvent then
			if inputObject.KeyCode == Enum.KeyCode.R then
				plane:rotate()
				orientation = dummyModel.PrimaryPart.Orientation.Y
			end
		end
	end)
end

initiatePlacement("Wall")

Here is the API / module:

To prevent people from scrolling in case they don't want to look at the API / Module
--[[
	Written by Tunicus
	Made 10/20/2017
	Model link: https://www.roblox.com/library/1122124371/Placement-Handler
	Youtube Video: Coming Soon (TM)
	Giving credit is optional
	
	V2 Changelog
		- Now aligns to any plane (designated by baseplate rotation)
		- Rotation tweening
		- Multi-place
		- Touch compatibility (for positioning)
		- plane.stateChanged signal for implementing custom changes during item collision or loading
		- plane:setLoading(bool loading)
		 	- when set to true, locks the model in place and prevents further placing (useful for waiting for remote function responses)
			- set to false to disable
		- plane:enable(array models, bool prealigned) *NOT COMPATIBLE WITH PREVIOUS VERSION, BE SURE TO EDIT!*
			- first argument is an array of items to place. Their positions and rotations relative to eachother designate their spacing when multi-placing
			- second argument is whether the array of items is already positioned according to the rotation of the plane
			
	Usage instructions
		This is a barebones module intended for placement handling ONLY, inventory management and alike are up to the developer. The goal here is initiate placement,
			and if the user places the item, return the location they placed it at
		
		To start with, require the module in a local script(obviously). I recommend placing it somewhere where it won't be reset on player death
			local placement = require(moduleObject)
			
		Now to create your placement object, you'll need 3 things
			Plane - Placement surfaces are designated as planes and are represented by a rectangular part
			Obstacles - A model or some other holder where all of the currently placed items are stored. Items here are expected to be models and have a primary part
			Grid Size - Size of your grid in studs, most sandbox tycoons use 3 (the plane's x and z size must be divisible by the grid size!)
		
		Once you have these three arguments, create your placement object
			local object = placement.new(plane, obstacles, grid)
			
		Now to initiate placement, you'll need an item. Items must be models, have a primary part, and their x and z size must be evenly divisible by your grid size
		For the sake of flexibility, the only properties affected during placement are Hitbox.Color, Hitbox.Transparency, Hitbox.Anchored, Hitbox.CanCollide, and the model's CFrame
		If you want other placement properties, such as the model being transparent or CanCollide false, it's your responsibility to set that prior to placement
		
		Now to initiate placement, call the enable method on your object with the model(s) as the argument in the form of an array (the model should probably be a clone)
			local onObjectPlaced = object:enable({models})
			
		You'll notice I'm storing the result as a variable, this is because the enable function returns a signal that fires whenever the item is successfully placed. In order to detect this, use the connect function,
			as you would any other signal such as Part.Touched or Player.PlayerAdded
			
				onObjectPlaced:connect(function(array CFrames) -- The array returns the CFrames of each of the models in the same order they were provided in the models table
					print(location)	
				end)
		
		If you kept the above code though, placement would continue. I can't stress this enough, this module is strictly for placement handling, YOU are responsible for the rest. First of all, when the user places an item,
			you might want to disable placement. 							
			
		To disable placement while it's active, do
			object:disable()
		or
			placement.disable()
			
		
		I'd imagine you actually want to place the model itself, and since you're a competent game developer that uses FilteringEnabled, you'll have to do this server sided. Here's some untested sample code to give you
			an idea of what to do
			
			SERVER SCRIPT
					local remoteEvent = Instance.new("RemoteEvent", game:GetService("ReplicatedStorage")
					remoteEvent.Name = "PlacementEvent"
					
					local itemBin = game:GetService("ReplicatedStorage").items -- folder full of all the items in the game				
					
					remoteEvent.OnServerEvent:connect(function(player, itemName, location)
						if [insert inventory check here] then
							local item = itemBin[itemName]:clone()
							item.Parent = [insert player's base model here]
							item:SetPrimaryPartCFrame(location)
						end
					end)
				
			LOCAL SCRIPT
				
					local placement = require(moduleObject)
					local object = placement.new(plane, obstacles, gridSize)
					
					local contextActionService = game:GetService("ContextActionService")
					local button = guiButton
					local textBox = textBox
					
					local itemBin = game:GetService("ReplicatedStorage").items
					local placementEvent = game:GetService("ReplicatedStorage"):WaitForChild("PlacementEvent")
					
					local placementModel = nil
					local placementActive = false
					
					local function endPlacement()
						if (placementActive) then
							object:disable()
							placementActive = false
							placementModel:Destroy()
							placementModel = nil
						end
					end				
					
					local function itemPlaced(locations)
						placementEvent:FireServer(placementModel.Name, locations[1])
						endPlacement()
					end			
					
					button.MouseButton1Click:connect(function()
						local item = itemBin:FindFirstChild(textBox.Text)
						
						if (item and not placementActive) then
							placementModel = item:clone()
							placementActive = true
							
							for _, obj in pairs(placementModel:GetChildren()) do
								if (obj:IsA("BasePart")) then
									obj.Anchored = true
									obj.CanCollide = false
									obj.Transparency = obj.Transparency + (1 - obj.Transparency) * .5
								end
							end
								
							object:enable({placementModel})
						end
					end)
					
					game:GetService("ContextActionService"):BindActionAtPriority("CancelPlacement", endPlacement, false, 10, Enum.KeyCode.X)
--]]

--[[
	CONSTANTS (FEEL FREE TO EDIT)
--]]

local INTERPOLATION = true -- whether placement smoothing is enabled
local INTERPOLATION_DAMP = .21 -- how fast smoothing is, make it a value (0, 1]
local ROTATION_SPEED = .2 -- rotation tween speed in seconds

local COLLISION_COLOR3 = BrickColor.Red().Color -- color of the hitbox when object collides
local COLLISION_TRANSPARENCY = .5 -- transparency of the hitbox when object collides

local NORMAL_COLOR3 = BrickColor.new().Color -- color of the hitbox
local NORMAL_TRANSPARENCY = .8 -- transparency of the hitbox

local LOAD_COLOR3 = Color3.fromRGB(226, 155, 64) -- color of the hitbox when loading
local LOAD_TRANSPARENCY = .6 -- transparency of the hitbox when loading

local GRID_TEXTURE = "rbxassetid://13786085" -- texture of the grid space, set to nil if you don't want a visible grid

local OVERRIDE_CONTROLS = false -- whether click to place and r to rotate are enabled
local INPUT_PRIORITY = 9 -- input priority of default controls
local TOUCH_RANGE = 30 -- range of mobile placement

local PLACEMENT_COOLDOWN = 1 -- how quickly the user can place


--[[
	DO NOT EDIT PAST THESE LINES UNLESS YOU KNOW WHAT YOU'RE DOING	
--]]

local module = {}

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local runService = game:GetService("RunService")
local touch = game:GetService("UserInputService").TouchEnabled

local currentPlane
local currentBase

local renderConnection = nil

local cx, cy, cz, cr = 0, 0, 0, 0
local currentObjects = {}
local currentEvent
local currentTexture
local currentPosition = Vector3.new(0, 0, 0)
local ax, az

local currentExtentsX
local currentExtentsZ
local currentYAxis
local currentXAxis
local currentZAxis
local currentAxis

local ox, oy, oz -- do
local dxx, dxy, dxz
local dzx, dzy, dzz

local min = math.min
local max = math.max
local abs = math.abs

local springVelocity = Vector3.new(0, 0, 0)
local springPosition = Vector3.new(0, 0, 0)
local tweenGoalRotation = 0 -- springs arent worth effort for rotation
local tweenStartRotation = 0
local tweenCurrentRotation = 0
local tweenAlpha = 1

local lastRenderCycle = tick()
local lastPlacement = tick()

local function round(n, to)
	return n % to ~= 0 and (n % to) > to/2 and (n + to - (n % to)) or (n - (n % to))	
end

local function project(px, py, pz)
	px, py, pz = px - ox, py - oy, pz - oz
	
	return px * dxx + py * dxy + pz * dxz, px * dzx + py * dzy + pz * dzz
end

local function translate(px, pz, r)
	if (r == 0) then
		return px, pz
	elseif (r == 1) then
		return -pz, px
	elseif (r == 2) then
		return -px, -pz
	elseif (r == 3) then
		return pz, -px
	end
	
--  or for all angles
--	r = r * math.pi/2
--	return math.cos(r) * px - math.sin(r) * pz,  math.sin(r) * px + math.cos(r) * pz	
end

local function angleLerp(a, b, d) -- to avoid angle jump
	local x1, y1 = math.cos(a), math.sin(a)
	local x2, y2 = math.cos(b), math.sin(b)
	
	return math.atan2(y1 + (y2 - y1) * d, x1 + (x2 - x1) * d)
end

local function calculateExtents(part, cf)
	local cf = cf or part.CFrame
	
	local edgeA = cf * CFrame.new(-part.Size.X/2, 0, 0)
	local edgeB = cf * CFrame.new(part.Size.X/2, 0, 0)
	local edgeC = cf * CFrame.new(0, 0, part.Size.Z/2)
	local edgeD = cf * CFrame.new(0, 0, -part.Size.Z/2)
	
	local edgeAx, edgeAz = project(edgeA.X, edgeA.Y, edgeA.Z)
	local edgeBx, edgeBz = project(edgeB.X, edgeB.Y, edgeB.Z)
	local edgeCx, edgeCz = project(edgeC.X, edgeC.Y, edgeC.Z)
	local edgeDx, edgeDz = project(edgeD.X, edgeD.Y, edgeD.Z)
	
	local extentsX = max(edgeAx, edgeBx, edgeCx, edgeDx) - min(edgeAx, edgeBx, edgeCx, edgeDx)
	local extentsZ = max(edgeAz, edgeBz, edgeCz, edgeDz) - min(edgeAz, edgeBz, edgeCz, edgeDz)
	
	return round(extentsX, currentPlane.grid), round(extentsZ, currentPlane.grid)
end

local states = {
	neutral = 1;
	collision = 2;
	loading = 3;
}

module.states = states

local function setState(object, state, downward)
	if (object.state and (object.state == state or (object.state > state and not downward))) then
		return
	end
	
	object.state = state
	
	if (state == 1) then
		if (NORMAL_COLOR3) then
			object.model.PrimaryPart.Color = NORMAL_COLOR3
		end
		if (NORMAL_TRANSPARENCY) then
			object.model.PrimaryPart.Transparency = NORMAL_TRANSPARENCY
		end
	elseif (state == 2) then
		if (COLLISION_COLOR3) then
			object.model.PrimaryPart.Color = COLLISION_COLOR3
		end
		if (COLLISION_TRANSPARENCY) then
			object.model.PrimaryPart.Transparency = COLLISION_TRANSPARENCY
		end
	elseif (state == 3) then
		if (LOAD_COLOR3) then
			object.model.PrimaryPart.Color = LOAD_COLOR3
		end
		if (LOAD_TRANSPARENCY) then
			object.model.PrimaryPart.Transparency = LOAD_TRANSPARENCY
		end
	end
	
	currentPlane.stateEvent:Fire(object.model, state)
end

--[[
	status
	model
	px, pz, r
	sx, sz
--]]

local function floatEqual(a, b)
	return (math.abs(a - b) < .01)
end

local function floatGreater(a, b)
	return (a - b) > .01
end

local function floatLesser(a, b)
	return (b - a) > .01
end

local function obstacleCollision()
	local px, pz = cx, cz
	
	local collision = false
	
	for _, model in pairs(currentPlane.obstacles:GetChildren()) do
		if (model.PrimaryPart) then
			
			local extentsX1, extentsZ1 = calculateExtents(model.PrimaryPart)
			local x1, z1 = project(model.PrimaryPart.Position.X, model.PrimaryPart.Position.Y, model.PrimaryPart.Position.Z)			
						
			for i = 1, #currentObjects do
				local object = currentObjects[i]
				
				if (not object.collision) then
					local r = (object.r + cr) % 4			
					
					local x0, z0 = translate(object.px, -object.pz, cr)
					x0 = x0 + px
					z0 = z0 + pz
					
					local extentsX0, extentsZ0				
								
					if (r == 1 or r == 3) then
						extentsX0, extentsZ0 = object.sz, object.sx
					else
						extentsX0, extentsZ0 = object.sx, object.sz
					end		
					
					if (floatLesser(x0 - extentsX0/2, x1 + extentsX1/2) and floatGreater(x0 + extentsX0/2, x1 - extentsX1/2) and floatLesser(z0 - extentsZ0/2, z1 + extentsZ1/2) and floatGreater(z0 + extentsZ0/2, z1 - extentsZ1/2)) then
						collision = true
						object.collision = true
						setState(object, states.collision)
					end
				end
			end
		end
	end
	
	for i = 1, #currentObjects do
		local object = currentObjects[i]
		
		if (not object.collision and object.state == states.collision) then
			setState(object, states.neutral, true)
		else
			object.collision = nil
		end
	end
	
	return not collision
end

local function inputCapture() -- converts user inputs to 3d position, built in mobile compatibility
	local position
	
	if (touch) then
		local camera = workspace.CurrentCamera
		local range = TOUCH_RANGE * (camera.Focus.p - camera.CFrame.p).magnitude/5	
		
		position = camera.Focus.p + camera.CFrame.lookVector * (camera.CoordinateFrame.lookVector.Y/2 + .5) * range
	else
		local hit, p = workspace:FindPartOnRayWithWhitelist(Ray.new(mouse.UnitRay.Origin, mouse.UnitRay.Direction * 999), {currentPlane.base})
		
		if (hit) then
			position = p
		end
	end
	
	return position, cr
end

local resting = false

local function calc(position, rotation, force)
	if (not currentPlane or currentPlane.loading) then
		return
	end
	
	force = force == true
	
	local ux, uz
	
	if (position) then
		ux, uz = project(position.X, position.Y, position.Z)
	else
		ux, uz = ax or 0, az or 0
	end
		 
	local nr = rotation or cr
		
	local nx = round(ux, currentPlane.grid)
	local nz = round(uz, currentPlane.grid)
	
	local extentsX, extentsZ = translate(currentExtentsX, currentExtentsZ, nr)
	extentsX = abs(extentsX)
	extentsZ = abs(extentsZ)
	
	if (floatEqual(extentsX/2 % currentPlane.grid, 0)) then
		nx = nx + currentPlane.grid/2
	end
	
	if (floatEqual(extentsZ/2 % currentPlane.grid, 0)) then
		nz = nz + currentPlane.grid/2
	end
	
	nx = nx + currentPlane.offsetX
	nz = nz + currentPlane.offsetZ
	
	local borderX = currentPlane.size.X/2
	local borderZ = currentPlane.size.Z/2
	
	ax = ux
	az = uz
	
	if (nx + extentsX/2 > borderX) then
		nx = nx - (nx + extentsX/2 - borderX)
	elseif (nx - extentsX/2 < -borderX) then
		nx = nx - (nx - extentsX/2 + borderX)
	end
	
	if (nz + extentsZ/2 > borderZ) then
		nz = nz - (nz + extentsZ/2 - borderZ)
	elseif (nz - extentsZ/2 < -borderZ) then
		nz = nz - (nz - extentsZ/2 + borderZ)
	end
	
	if (force or nx ~= cx or nz ~= cz or cr ~= nr) then
		cx, cz, cr = nx, nz, nr
		resting = false
		currentPosition = (Vector3.new(ox, oy, oz) + currentXAxis * nx + currentZAxis * nz) 
		obstacleCollision()
	end
end

local function render()
	if (resting) then
		return
	end
	
	if (INTERPOLATION) then
		local delta = 1/60
		
		springVelocity = (springVelocity + (currentPosition - springPosition)) * INTERPOLATION_DAMP * 60 * delta
		springPosition = springPosition + springVelocity
		
		local extentsX, extentsZ = translate(currentExtentsX, currentExtentsZ, cr)
		
		local vx, vz = 9 * springVelocity:Dot(currentXAxis)/abs(extentsX), 9 * springVelocity:Dot(currentZAxis)/abs(extentsZ)
		local r
		
		if (not floatEqual(tweenGoalRotation, cr * math.pi/2)) then
			tweenStartRotation = tweenCurrentRotation
			tweenGoalRotation = cr * math.pi/2
			tweenAlpha = 0
		end
				
		if (tweenAlpha < 1) then
			tweenAlpha = min(1, tweenAlpha + delta/ROTATION_SPEED)
			tweenCurrentRotation = angleLerp(tweenStartRotation, tweenGoalRotation, 1 - (1 - tweenAlpha)^2)
			r = tweenCurrentRotation
		else
			r = cr * math.pi/2
		end
		
		
		local effectAngle = CFrame.Angles(math.sqrt(math.abs(vz/100)) * math.sign(vz), 0, math.sqrt(math.abs(vx/100)) * math.sign(vx))
		
		local rotationCFrame = currentAxis * effectAngle * CFrame.Angles(0, r, 0)
		local centerCFrame = rotationCFrame + springPosition
		
		for i = 1, #currentObjects do
			local object = currentObjects[i]
						
			local x, z = object.px, object.pz			
			
			object.model:SetPrimaryPartCFrame(centerCFrame * CFrame.Angles(0, object.r * math.pi/2, 0) + rotationCFrame * Vector3.new(x, object.sy/2, z))
		end		
		
		if (springVelocity.magnitude < .01 and tweenAlpha >= 1) then
			resting = true
		end
	else
		local rotationCFrame = currentAxis * CFrame.Angles(0, cr * math.pi/2, 0)		
		
		for i = 1, #currentObjects do
			local object = currentObjects[i]
			local x, z = object.px, object.pz
			
			object.model:SetPrimaryPartCFrame(rotationCFrame * CFrame.Angles(0, object.r * math.pi/2, 0) + rotationCFrame * Vector3.new(x, object.sy/2, z) + currentPosition)
		end
		
		resting = true
	end
end

local function run(display)
	local position, rotation = inputCapture()
	
	if (position or rotation) then
		calc(position, rotation)
		if (display) then
			render()
		end
	end
end

local function place()
	if (currentPlane and (obstacleCollision()) and (tick() - lastPlacement) >= PLACEMENT_COOLDOWN and not currentPlane.loading) then
		lastPlacement = tick()
				
		local modelCFrames = {}
		
		for i = 1, #currentObjects do
			local object = currentObjects[i]
			modelCFrames[i] = currentAxis * CFrame.Angles(0, (object.r + cr) * math.pi/2, 0) +  (currentAxis * CFrame.Angles(0, cr * math.pi/2, 0)) * Vector3.new(object.px, object.sy/2, object.pz) + currentPosition
		end
		
		currentEvent:Fire(modelCFrames)		
	end
end

local function rotate()
	calc(nil, (cr + 1) % 4)
	render()
end

local function inputRotate(_, userInputState, inputObject)
	if (currentPlane and userInputState == Enum.UserInputState.End) then
		rotate()
	end
end

local function inputPlace(_, userInputState, inputObject)
	if (currentPlane and userInputState == Enum.UserInputState.End) then
		place()
	end
end

local function bindInputs()
	if (not OVERRIDE_CONTROLS) then
		game:GetService("ContextActionService"):BindActionAtPriority("PlacementModule_Rotate", inputRotate, false, INPUT_PRIORITY, Enum.KeyCode.R)
		game:GetService("ContextActionService"):BindActionAtPriority("PlacementModule_Place", inputPlace, false, INPUT_PRIORITY, Enum.UserInputType.MouseButton1)
	end
end

local function unbindInputs()
	if (not OVERRIDE_CONTROLS) then
		game:GetService("ContextActionService"):UnbindAction("PlacementModule_Rotate")
		game:GetService("ContextActionService"):UnbindAction("PlacementModule_Place")
	end
end

local function enablePlacement(plane, models, prealigned)
	if (plane == currentPlane) then
		return
	elseif (currentPlane) then
		currentPlane:disable()
	end
	
	if (type(models) ~= "table") then
		models = {models}
	end
	
	lastRenderCycle = tick()
	
	currentPlane = plane
	currentBase = plane.base

	local planePosition = currentBase.CFrame * Vector3.new(0, currentBase.Size.Y/2, 0)	
	
	resting = false
	ox, oy, oz = planePosition.X, planePosition.Y, planePosition.Z
	currentXAxis = currentBase.CFrame.rightVector
	currentYAxis = currentBase.CFrame.upVector
	currentZAxis = currentBase.CFrame.lookVector
	currentAxis = CFrame.new(0, 0, 0, currentXAxis.X, currentYAxis.X, -currentZAxis.X, currentXAxis.Y, currentYAxis.Y, -currentZAxis.Y, currentXAxis.Z, currentYAxis.Z, -currentZAxis.Z)
	currentObjects = {}
	dxx, dxy, dxz = currentXAxis.X, currentXAxis.Y, currentXAxis.Z
	dzx, dzy, dzz = currentZAxis.X, currentZAxis.Y, currentZAxis.Z
	cx, cy, cz, cr = 0, 0, 0, 0
	
	springVelocity = Vector3.new(0, 0, 0)
	springPosition = Vector3.new(0, 0, 0)
	
	tweenAlpha = 0
	tweenCurrentRotation = 0
	tweenGoalRotation = 0
	tweenStartRotation = 0
	
	local position, _ = inputCapture()
	
	if (not position) then
		position = planePosition
	end
	
	springPosition = position
	
	do
		local extentsXMin, extentsXMax = 10e10, -10e10
		local extentsZMin, extentsZMax = 10e10, -10e10
		
		for i = 1, #models do
			local model = models[i]
			local object = {}
			object.model = model
			
			local lookVector = object.model.PrimaryPart.CFrame.lookVector	
			local theta
			local px, pz
			
			if (prealigned) then
				local position = object.model.PrimaryPart.CFrame * Vector3.new(0, -object.model.PrimaryPart.Size.Y/2 , 0)		
				px, pz = project(position.X, position.Y, position.Z)

				theta = math.acos(math.clamp(lookVector:Dot(currentZAxis), -1, 1))
				local cross = lookVector:Cross(currentZAxis)
				
				if (cross:Dot(currentYAxis) > 0) then
					theta = -theta
				end
				
			else
				px, pz = object.model.PrimaryPart.Position.X, object.model.PrimaryPart.Position.Z
				theta = math.atan2(lookVector.X, lookVector.Z)		
			end
			
			local x, z = model.PrimaryPart.Size.X, model.PrimaryPart.Size.Z
			
			object.r = round((theta % (2 * math.pi))/(math.pi/2), 1)
			
			if (object.r == 1 or object.r == 3) then
				x, z = z, x
			end
			
			local x1, x2 = px + x/2, px - x/2
			local z1, z2 = pz + z/2, pz - z/2	
			
			if (x2 < extentsXMin) then
				extentsXMin = x2
			end
			if (x1 > extentsXMax) then
				extentsXMax = x1
			end
			
			if (z2 < extentsZMin) then
				extentsZMin = z2
			end
			if (z1 > extentsZMax) then
				extentsZMax = z1
			end
						
			
			model.PrimaryPart.Transparency = .5
			model.PrimaryPart.Anchored = true
			model.PrimaryPart.CanCollide = false
			
			setState(object, plane.loading and states.loading or states.neutral)
			
			currentObjects[i] = object
		end
		
		currentExtentsX, currentExtentsZ = round(extentsXMax - extentsXMin, currentPlane.grid), round(extentsZMax - extentsZMin, currentPlane.grid)
				
		for i = 1, #currentObjects do
			local object = currentObjects[i]
			local px, pz
			
			if (prealigned) then
				local position = object.model.PrimaryPart.CFrame * Vector3.new(0, -object.model.PrimaryPart.Size.Y/2, 0)		
				px, pz = project(position.X, position.Y, position.Z)
			else
				px, pz = object.model.PrimaryPart.Position.X, object.model.PrimaryPart.Position.Z
			end
			
			object.px = px - (extentsXMin + extentsXMax)/2
			object.pz = (pz - (extentsZMin + extentsZMax)/2) * (prealigned and -1 or 1)
									
			object.sx, object.sy, object.sz = object.model.PrimaryPart.Size.X, object.model.PrimaryPart.Size.Y, object.model.PrimaryPart.Size.Z
		end
	end
	
	renderConnection = runService.RenderStepped:connect(run)
	module.currentPlane = currentPlane
	currentEvent = Instance.new("BindableEvent")
	
	if (GRID_TEXTURE) then
		currentTexture = Instance.new("Texture", currentPlane.base)
		currentTexture.Texture = GRID_TEXTURE
		currentTexture.Face = Enum.NormalId.Top
		currentTexture.StudsPerTileU = currentPlane.grid
		currentTexture.StudsPerTileV = currentPlane.grid
	end
		
	bindInputs()
	
	run(true)
		
	return currentEvent.Event
end

local function disablePlacement(plane)
	if (currentPlane) then
		renderConnection:disconnect()
		renderConnection = nil
		currentPlane = nil
		module.currentPlane = nil
		currentEvent:Destroy()
		currentEvent = nil	
		
		if (currentTexture) then
			currentTexture:Destroy()
			currentTexture = nil
		end
		
		for i = 1, #currentObjects do
			local object = currentObjects[i]
			object.model = nil
			currentObjects[i] = nil
		end
		
		unbindInputs()
	end
end

local function setLoading(plane, isLoading)
	if (plane.loading == isLoading) then
		return
	end
	
	plane.loading = isLoading
	
	if (plane == currentPlane) then	
		if (isLoading) then
			for i = 1, #currentObjects do
				setState(currentObjects[i], states.loading)
			end
		else
			for i = 1, #currentObjects do
				setState(currentObjects[i], states.neutral, true)
			end
			
			obstacleCollision()
		end
	end
end

module.new = function(base, obstacles, grid)
	local plane = {}
	plane.base = base
	plane.obstacles = obstacles
	plane.position = base.Position
	plane.size = base.Size
	
	if (math.floor(.5 + plane.size.X/grid) % 2 == 0) then
		plane.offsetX = -grid/2
	else
		plane.offsetX = 0
	end
	
	if (math.floor(.5 + plane.size.Z/grid) % 2 == 0) then
		plane.offsetZ = -grid/2
	else
		plane.offsetZ = 0
	end
	
	
	plane.stateEvent = Instance.new("BindableEvent")
	plane.stateChanged = plane.stateEvent.Event	
	
	plane.grid = grid
	plane.enable = enablePlacement
	plane.disable = disablePlacement
	plane.rotate = rotate
	plane.place = place
	plane.setLoading = setLoading
	
	return plane
end

module.setLoading = setLoading
module.currentPlane = false

return module

Lastly, here is the server script:


local items = game:GetService("ReplicatedStorage").Items
game:GetService("ReplicatedStorage").ClientPlaced.OnServerEvent:Connect(function(player, itemName, location, rotation, base)
	local itemTemplate = items:FindFirstChild(itemName)

	if (itemTemplate) then
		if (base.Name == player.Name) then
			local item = itemTemplate:clone()
			item.Parent = base.ItemHolder
			item:SetPrimaryPartCFrame(location)
			item:SetPrimaryPartCFrame(item.PrimaryPart.CFrame * CFrame.Angles(0, math.rad(rotation), 0))
		end
	end
end)

I want to get this basic rotation system done, and I am dying to get this issue fixed. Please help me!

1 Like

Before I get to the solution I have a few suggestions:
I’ve seen many users complain about performance issues with SetPrimaryPartCFrame and they recommended directly setting CFrame although I’m not sure what the accuracy of these claims are. It might make more sense to merge the two lines as well (item.PrimaryPart.CFrame = location * CFrame.Angles(0, math.rad(rotation), 0)).

Your problem is I believe actually not the lack of rotation but rather you apply the rotation twice. 90 + 90 = 180 so your model is flipped. This is because the location cframe appears to have rotation included already. To fix this issue you need to cancel out rotation of location like so: location = CFrame.new(location.p).

1 Like

Removing that line of code that I made which was ment to rotate the model fixed the issue entirely :eyes: !

Thank you sooo much for helping me get this fixed!

1 Like