Placement preview model overlaps with basepart when placed against a vertical surface

I’m having trouble with an item placement rendering-preview mechanism — the sides of the item trying to be placed are overlapping with other parts. As of right now, the item shouldn’t be able to be placed directly against a vertical surface (wall, terrain, etc.) but it should not render the item through a part regardless.

The issue looks like this in live preview render in live play:

And the goal is to make it look like the green part on the right (see illustration below)

The source code right now is as follows

-- this function below is binded to render step in another script, this code below is where the bug symptom is at
function RenderPlacement(placementObject: Model)
	Mouse.TargetFilter = placementObject -- set mouse target filter to the placement object so that it doesn't collide with itself
	self:SetPlacementColor(placementObject, self.BLUEPRINT_COLOR, Enum.Material.ForceField)
	
	if Mouse.Target then
		
		local rootPosition = Character.HumanoidRootPart.Position
		local difference = rootPosition - Mouse.Hit.Position
		local distance = math.clamp(difference.Magnitude, self.MIN_DISTANCE, self.MAX_DISTANCE)
		local newMouseHit = -(difference.Unit * distance) + rootPosition

		local raycastParams = RaycastParams.new()
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = { placementObject, Character, Mouse }

		-- cast our ray and get our normal, which is a unit vector
		local ray = Ray.new(placementObject.PrimaryPart.Position, Vector3.new(0, -1, 0) * 100)
		local result = workspace:Raycast(ray.Origin, ray.Direction, raycastParams)
		
		if result then
			local normal = result.Normal

			-- get the perpendicular vector and the angle between the two
			local cross = Vector3.new(0, 1, 0):Cross(normal)
			local theta = math.acos(Vector3.new(0, 1, 0):Dot(normal))

			-- make sure our axis is valid
			local axis = Vector3.new(1, 0, 0)
			if cross.Magnitude > 0 then
				axis = cross.Unit
			end
			
			-- get the model's size and cframe
			local size, orientation = placementObject:GetBoundingBox()
			
			if distance <= self.MIN_DISTANCE then
				self:SetPlacementColor(placementObject, self.ERROR_COLOR, Enum.Material.ForceField)

				placementObject:PivotTo(
					CFrame.new(newMouseHit + normal * orientation.Y / 2)
						* CFrame.new(0, 0, 0)
						* CFrame.Angles(0, math.rad(Character:WaitForChild("HumanoidRootPart").Orientation.Y), 0)
						* CFrame.Angles(0, math.rad(self.placementInfo.rotateValue), 0)
				)
				self.placementInfo.cFrame = placementObject:GetPivot()
				return
				
			end
			
			if distance >= self.MAX_DISTANCE then
				-- Don't rotate the model to terrain surface
				self:SetPlacementColor(placementObject, self.ERROR_COLOR, Enum.Material.ForceField)
				self.placementInfo.canPlace = false

				normal = Vector3.new(0, 1, 0)
				placementObject:PivotTo(
					CFrame.new(newMouseHit + normal * orientation.Y / 2)
						* CFrame.new(0, 0, 0)
						* CFrame.Angles(0, math.rad(Character.HumanoidRootPart.Orientation.Y), 0)
						* CFrame.Angles(0, math.rad(self.placementInfo.rotateValue), 0)
				)
				self.placementInfo.cFrame = placementObject:GetPivot()

				return
			end

			-- CFrame the model
			self.placementInfo.canPlace = true
			self.placementInfo.cFrame = placementObject:GetPivot()
			placementObject:PivotTo(
				CFrame.new(newMouseHit + normal * orientation.Y / 2)
					* CFrame.fromAxisAngle(axis, theta):ToWorldSpace(
						CFrame.new(0, 0, 0)
							* CFrame.Angles(0, math.rad(Character:WaitForChild("HumanoidRootPart").Orientation.Y), 0)
							* CFrame.Angles(0, math.rad(self.placementInfo.rotateValue), 0)
					)
			)

			-- check for collision with other objects, if there is a collision then prevent item from being placed
			local hitbox = placementObject:FindFirstChild("Hitbox")

			local overlapParams = OverlapParams.new()
			overlapParams.FilterType = Enum.RaycastFilterType.Exclude
			overlapParams.FilterDescendantsInstances = { hitbox, placementObject }
			local overlappingParts = workspace:GetPartBoundsInBox(hitbox.CFrame, hitbox.Size, overlapParams)
			
			if #overlappingParts > 0 then
				for _, overlappingPart: BasePart in pairs(overlappingParts) do
					if overlappingPart then
						self:SetPlacementColor(placementObject, self.ERROR_COLOR, Enum.Material.ForceField)
						self.placementInfo.canPlace = false
					end
				end
			end

		end
	else
		-- Mouse does not have a target, set the model to the mouse position in relation to the max distance allowed
		self:SetPlacementColor(placementObject, self.ERROR_COLOR, Enum.Material.ForceField)
		self.placementInfo.canPlace = false
		
		local rootPosition = Character.HumanoidRootPart.Position
		local difference = rootPosition - Mouse.Hit.Position
		local distance = math.clamp(difference.Magnitude, self.MIN_DISTANCE, self.MAX_DISTANCE)
		local newMouseHit = -(difference.Unit * distance) + rootPosition
		
		placementObject:PivotTo(
			CFrame.new(newMouseHit + Vector3.new(0, 1, 0) * placementObject:GetExtentsSize().Y / 2)
				* CFrame.new(0, 0, 0)
				* CFrame.Angles(0, math.rad(Character:WaitForChild("HumanoidRootPart").Orientation.Y), 0)
				* CFrame.Angles(0, math.rad(self.placementInfo.rotateValue), 0)
		)

		self.placementInfo.cFrame = placementObject:GetPivot()

		return
	end
end

This is what I used for my placement system. It works fine, I’m not sure if its very efficient, though. You can try something similar to this and see if it works.

local Touched = PrimaryPart.Touched:Connect(function() end)
		local CollisionPoints = PrimaryPart:GetTouchingParts()
		for i =1,#CollisionPoints do
			if not CollisionPoints[i]:IsDescendantOf(Object) and not CollisionPoints[i]:IsDescendantOf(Character) then
				Collided = true
				break
			end
		end
		Touched:Disconnect()

The PrimaryPart Value would be the objects hitbox.

This doesn’t solve the issue. The model is supposed to touch the basepart, but isn’t supposed to go through it. Furthermore, we don’t want to break in that situation, it is a live-render preview that the client is doing.

1 Like

It might just be how you have your hitbox or also how you’re positioning the models CFrame.
Heres something that works for me.

local function CalculateYPosition(ToP,ToS,oS)
	return (ToP+ToS*.5)+oS*.5
end

ToP is the Plots Position on the Y Axis, ToS is the Plots Size On the Y Axis, and oS is the hitboxes Size on the Y Axis. Maybe you can try implementing something like this into your code that would help.