Parts clipping in placement system

  1. What do you want to achieve? Keep it simple and clear!
    I’m trying to make a placement system (like the one in build a boat for treasure), but the parts clip into eachother when placing them.

  2. What is the issue? Include screenshots / videos if possible!
    Current behaviour:
    Animation
    Look closely, the parts clip one stud into eachother when placed vertically.
    Expected behaviour:
    Animation

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I do not know where to look on the dev hub or elsewhere.

Here is the script. I think I need to tweak getPlacementCFrame() so that the normal also includes the parts length or so, but I do not know how to do that.

--!strict
local collectionService: CollectionService = game:GetService("CollectionService")
local replicatedStorage: ReplicatedStorage = game:GetService("ReplicatedStorage")

local mouse: Mouse = game.Players.LocalPlayer:GetMouse()
local dragger: Dragger = Instance.new("Dragger")
local tool: Tool = script.Parent
local dragClone: Model?
local raycastResult: RaycastResult?
local camera: Camera = workspace.CurrentCamera

local excludeList: {Instance} = {} 
local equipped: boolean = false
local _time = os.clock()

function getPlacementCFrame(raycastResult: RaycastResult): CFrame
	return CFrame.new(raycastResult.Position+raycastResult.Normal)
end

function createDragClone()
	dragClone = replicatedStorage.Placables.Part:Clone()
	dragClone.Parent = workspace
	for i, instance: Instance in pairs(dragClone:GetDescendants())  do
		if instance:IsA("BasePart") then
			instance.Transparency = 0.5
		end
	end
end

tool.Equipped:Connect(function()
	equipped = true
	createDragClone()
end)

tool.Unequipped:Connect(function()
	if dragClone then
		dragClone:Destroy()
		dragClone = nil
	end
	equipped = false
end)

mouse.Button1Down:Connect(function()
	_time = os.clock()
end)

mouse.Button1Up:Connect(function()
	--If the player held mouse button down for more than 0.25 secs then they probably want to cancel placing it
	if raycastResult and equipped and os.clock()-_time<0.25 then
		local pressSound: Sound = tool.PressSound:Clone()
		pressSound.Parent = tool
		pressSound:Play()
		game:GetService("Debris"):AddItem(pressSound, pressSound.TimeLength)
		
		local clone: Model = replicatedStorage.Placables.Part:Clone()
		clone.Parent = workspace
		clone:PivotTo(getPlacementCFrame(raycastResult))
	end
end)

mouse.Move:Connect(function()
	if raycastResult and dragClone then
		dragClone:PivotTo(getPlacementCFrame(raycastResult))
	end
end)

while task.wait() do
	local raycastParams: RaycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {dragClone}
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	
	table.clear(excludeList)
	for i, placable: Instance in pairs(collectionService:GetTagged("placable")) do
		for i, instance:Instance in pairs(placable:GetDescendants()) do
			if instance.Name ~= "HitBox" then
				table.insert(excludeList, instance)
			end
		end
	end
	
	local unitRay: Ray = camera:ScreenPointToRay(mouse.X, mouse.Y)
	raycastResult = workspace:Raycast(unitRay.Origin, unitRay.Direction * 500, raycastParams)
	
	if not raycastResult and dragClone then
		dragClone:Destroy()
		dragClone = nil
	end

	if not dragClone and equipped then
		createDragClone()
	end
end

If possible, could you also show how rotation like this could be achieved?
Animation

Also, since all of the blocks and items it’s going to have to place are in reality Models, that means that I don’t think i can use the Dragger object.

Sorry for the bump, but I think the answer might be usefull to know if other people happen to have the same problem, anyone got anything?

You can try using workspace:GetPartsInPart(<params>, <your part>) when a projection is shown and you could change the parts position relative to the mouse so that the part doesn’t clip.

I’m sure that would not be a reliable solution. I don’t want to only throw computational power at this problem to solve it. Below are a few diagrams that convey my problem better

@chillthrill709 Do you know how to solve this?
Thanks in advance

Here’s the updated function by the way, which only does step one

function getPlacementCFrame(raycastResult: RaycastResult, size: Vector3): CFrame
	local function normalToFaceID(normalVector, part): Enum.NormalId?
		local function getNormalFromFace(part: BasePart, normalID: Enum.NormalId)
			return part.CFrame:VectorToWorldSpace(Vector3.FromNormalId(normalID))
		end

		local tolerance: number = 1 - 0.001 --Floating point error
		local allFaceNormalIDs: {Enum.NormalId} = {
			Enum.NormalId.Front,
			Enum.NormalId.Back,
			Enum.NormalId.Bottom,
			Enum.NormalId.Top,
			Enum.NormalId.Left,
			Enum.NormalId.Right
		}    

		for _, normalID: Enum.NormalId in pairs( allFaceNormalIDs ) do
			if getNormalFromFace(part, normalID):Dot(normalVector) > tolerance then
				return normalID
			end
		end

		return nil
	end
		
	local placementCFrame: CFrame = CFrame.new(raycastResult.Position)
	local normalID: Enum.NormalId? = normalToFaceID(raycastResult.Normal, raycastResult.Instance)
	if normalID then
		if normalID == Enum.NormalId.Front then
			placementCFrame = placementCFrame+raycastResult.Normal*size.Z/2
		end
		if normalID == Enum.NormalId.Back then
			placementCFrame = placementCFrame+raycastResult.Normal*size.Z/2
		end
		if normalID == Enum.NormalId.Right then
			placementCFrame = placementCFrame+raycastResult.Normal*size.X/2
		end
		if normalID == Enum.NormalId.Left then
			placementCFrame = placementCFrame+raycastResult.Normal*size.X/2
		end
		if normalID == Enum.NormalId.Top then
			placementCFrame = placementCFrame+raycastResult.Normal*size.Y/2
		end
		if normalID == Enum.NormalId.Bottom then
			placementCFrame = placementCFrame+raycastResult.Normal*size.Y/2
		end
	else
		return CFrame.new()
	end
	
	return placementCFrame
end

The problem is that roblox doesn’t give you much control over the 3D engine. The solution you proposed could work, computational power will still be needed. I’ll still help as much as I can.

No, that’s not the problem. I looked into the client-side deobfuscated localscript for the build tool of babft to try to help me (from someone else!!) and he did some mathematical wizardry to figure it out. This is the piece of code in question, although all of the variable names are hidden.

	if not l__mouse__20.Target then
		if u19 then
			return;
		end;
	end;
	if not p2 then
		u19 = l__mouse__20.Target;
		u21 = l__mouse__20.Hit;
		u22 = l__mouse__20.TargetSurface;
	end;
	local v15 = (u19.CFrame - u19.Position):Inverse();
	if l__LocalPlayer__2.PlayerGui.BuildGui.InventoryFrame.MoreFrame.RelativeRotation.RelativeRotation.Value then
		v15 = CFrame.new(0, 0, 0);
	end;
	local v16, v17 = normalIdToNormalVector(u22, u19);
	local v18 = u19.CFrame * CFrame.new(v16 * (v17 / 2));
	u1:SetPrimaryPartCFrame(v18 * v15 * u23);
	local v19 = (u19.CFrame - u19.Position):PointToWorldSpace(v16);
	local v20 = math.abs((u1.PrimaryPart.CFrame.RightVector:Dot(v19)));
	local v21 = math.abs((u1.PrimaryPart.CFrame.UpVector:Dot(v19)));
	local v22 = math.abs((u1.PrimaryPart.CFrame.LookVector:Dot(v19)));
	u1:SetPrimaryPartCFrame(v18 * v15:Inverse() * u23:Inverse());
	local v23 = u19.CFrame:PointToObjectSpace(u21.p);
	if u22 ~= Enum.NormalId.Top then
		if u22 == Enum.NormalId.Bottom then
			local v24 = v23 - Vector3.new(0, v23.Y, 0);
		elseif u22 ~= Enum.NormalId.Front then
			if u22 == Enum.NormalId.Back then
				v24 = v23 - Vector3.new(0, 0, v23.Z);
			else
				v24 = v23 - Vector3.new(v23.X, 0, 0);
			end;
		else
			v24 = v23 - Vector3.new(0, 0, v23.Z);
		end;
	else
		v24 = v23 - Vector3.new(0, v23.Y, 0);
	end;
	local v25 = tonumber(l__LocalPlayer__2.PlayerGui.BuildGui.InventoryFrame.MoreFrame.Move.TextLabel.Text);
	if v25 ~= 0 then
		v24 = roundVectorToBase(v24, v25);
	end;
	u1:SetPrimaryPartCFrame(v18 * CFrame.new(v16 * (v20 * (u1.PrimaryPart.Size.X / 2) + v21 * (u1.PrimaryPart.Size.Y / 2) + v22 * (u1.PrimaryPart.Size.Z / 2))) * CFrame.new(v24) * v15 * u23);

For the record, I have no idea what he did here.

Mathematical wizardry = computational power
Good luck though.

You’re sort of right. I botched up this script, and it’s reasonably performant, and works pretty good, so yeah. But I don’t think this is the answer. It’s just using a lot of brute force.

function getPlacementCFrame(raycastResult: RaycastResult, size: Vector3): CFrame
	local function normalToFaceIDRelativeToWorld(normalVector, part): Enum.NormalId?
		local closestGuessDot: number = 0
		local closestGuessNormalID: Enum.NormalId
		local allFaceNormalIDs: {Enum.NormalId} = {
			Enum.NormalId.Front,
			Enum.NormalId.Back,
			Enum.NormalId.Bottom,
			Enum.NormalId.Top,
			Enum.NormalId.Left,
			Enum.NormalId.Right
		}    

		for _, normalID: Enum.NormalId in pairs( allFaceNormalIDs ) do
			if Vector3.FromNormalId(normalID):Dot(normalVector) > closestGuessDot then
				closestGuessDot = Vector3.FromNormalId(normalID):Dot(normalVector)
				closestGuessNormalID = normalID
			end
		end

		return closestGuessNormalID
	end
	
	local function resolveColision(normalID: Enum.NormalId, movingPart: Part, movingPartPosition: Vector3, stationaryPart: Part): Vector3
		local simulationHitBox: Part = movingPart:Clone()
		simulationHitBox.CanCollide = false
		simulationHitBox.Position = movingPartPosition
		simulationHitBox.Parent = workspace
		
		local overlapParams: OverlapParams = OverlapParams.new()
		overlapParams.FilterType = Enum.RaycastFilterType.Include
		overlapParams.FilterDescendantsInstances = {simulationHitBox}
		
		local currentPosition: Vector3
		local currentDirection: Vector3 = Vector3.FromNormalId(normalID)
		for i = 1, 100 do
			simulationHitBox.Position = simulationHitBox.Position + currentDirection
			if #workspace:GetPartsInPart(stationaryPart, overlapParams) == 0 then
				simulationHitBox.Position = simulationHitBox.Position - currentDirection
				currentDirection /= 2
			end
		end
		
		local resolvedPosition = simulationHitBox.Position
		simulationHitBox:Destroy()
		return resolvedPosition
	end
		
	local placementCFrame: CFrame = CFrame.new(raycastResult.Position)
	local normalID: Enum.NormalId? = normalToFaceID(raycastResult.Normal, raycastResult.Instance)
	if normalID then
		placementCFrame = CFrame.new(resolveColision(normalID, dragClone.HitBox ,raycastResult.Position , raycastResult.Instance))
	else
		return CFrame.new()
	end
	
	return placementCFrame
end

EDIT: check it out!
Animation
UPDATED CODE:

--!strict
local collectionService: CollectionService = game:GetService("CollectionService")
local replicatedStorage: ReplicatedStorage = game:GetService("ReplicatedStorage")

local mouse: Mouse = game.Players.LocalPlayer:GetMouse()
local dragger: Dragger = Instance.new("Dragger")
local tool: Tool = script.Parent
local dragClone: Model?
local raycastResult: RaycastResult?
local camera: Camera = workspace.CurrentCamera

local excludeList: {Instance} = {} 
local equipped: boolean = false
local _time = os.clock()

function getPlacementCFrame(): CFrame
	assert(raycastResult, "RayCastResult is nil!")
	assert(dragClone, "DragClone is nil!")

	local function resolveColision(normal: Vector3, movingPart: Part, movingPartPosition: Vector3, stationaryPart: Part): CFrame
		local resolvedCFrame: CFrame = CFrame.Angles(math.rad(stationaryPart.Rotation.X), math.rad(stationaryPart.Rotation.Y), math.rad(stationaryPart.Rotation.Z))
		
		local simulationHitBox: Part = movingPart:Clone()
		simulationHitBox.CanCollide = false
		simulationHitBox.Position = movingPartPosition
		simulationHitBox.Parent = workspace
		
		local overlapParams: OverlapParams = OverlapParams.new()
		overlapParams.FilterType = Enum.RaycastFilterType.Include
		overlapParams.FilterDescendantsInstances = {simulationHitBox}
		
		local currentPosition: Vector3
		local currentDirection: Vector3 = normal
		for i = 1, 100 do
			simulationHitBox.Position = simulationHitBox.Position + currentDirection
			if #workspace:GetPartsInPart(stationaryPart, overlapParams) == 0 then
				simulationHitBox.Position = simulationHitBox.Position - currentDirection
				currentDirection /= 2
			end
		end
		
		resolvedCFrame = resolvedCFrame + simulationHitBox.Position
		simulationHitBox:Destroy()
		return resolvedCFrame
	end
		
	--local normalID: Enum.NormalId? = normalToFaceIDRelativeToWorld(raycastResult.Normal, raycastResult.Instance)
	local placementCFrame = resolveColision(raycastResult.Normal, dragClone.HitBox ,raycastResult.Position , raycastResult.Instance)

	return placementCFrame
end

function createDragClone()
	dragClone = replicatedStorage.Placables.Part:Clone()
	dragClone.Parent = workspace
	for i, instance: Instance in pairs(dragClone:GetDescendants())  do
		if instance:IsA("BasePart") then
			instance.Transparency = 0.5
		end
	end
end

tool.Equipped:Connect(function()
	equipped = true
	createDragClone()
end)

tool.Unequipped:Connect(function()
	if dragClone then
		dragClone:Destroy()
		dragClone = nil
	end
	equipped = false
end)

mouse.Button1Down:Connect(function()
	_time = os.clock()
end)

mouse.Button1Up:Connect(function()
	--If the player held mouse button down for more than 0.25 secs then they probably want to cancel placing it
	if raycastResult and equipped and os.clock()-_time<0.25 then
		local pressSound: Sound = tool.PressSound:Clone()
		pressSound.Parent = tool
		pressSound:Play()
		game:GetService("Debris"):AddItem(pressSound, pressSound.TimeLength)
		
		local clone: Model = replicatedStorage.Placables.Part:Clone()
		clone.Parent = workspace
		clone:PivotTo(getPlacementCFrame(raycastResult, clone.HitBox.Size))
	end
end)

mouse.Move:Connect(function()
	if raycastResult and dragClone then
		dragClone:PivotTo(getPlacementCFrame(raycastResult, dragClone.HitBox.Size))
	end
end)

while task.wait() do
	local raycastParams: RaycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {dragClone}
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	
	table.clear(excludeList)
	for i, placable: Instance in pairs(collectionService:GetTagged("placable")) do
		for i, instance:Instance in pairs(placable:GetDescendants()) do
			if instance.Name ~= "HitBox" then
				table.insert(excludeList, instance)
			end
		end
	end
	
	local unitRay: Ray = camera:ScreenPointToRay(mouse.X, mouse.Y)
	raycastResult = workspace:Raycast(unitRay.Origin, unitRay.Direction * 500, raycastParams)
	
	if not raycastResult and dragClone then
		dragClone:Destroy()
		dragClone = nil
	end

	if not dragClone and equipped and raycastResult then
		createDragClone()
	end
end

Please help. The function I used here isn’t reliable and doesn’t work in certain situations. I looked around and a lot of people are having the same problem with parts clipping into the ground with dragging systems.

its an easy fix, you should just snap the part to a grid and make sure the targets cframe is also snapped to the same grid

What are you talking about? In all instances I showed it not being snapped to a grid. Also, it would still not fix the problem

BEHOLD!!! the ULTIMATE DRAGGING SYSTEM! (well, not really. If you’re able to help out please look at the TODO line in getPlacementCFrame in the build tool)
Animation
Below is the place file.
Untitled-Game.rbxl (850.7 KB)

Thank you for making the game open source! This is an interesting solution you came up with!

no problem (: i made a better solution a few hours later
but with partcasting soon to be introduced it’ll soon become obsolete, too
here’s the updated code (i won’t reply to this post anymore)

--!nonstrict
local collectionService: CollectionService = game:GetService("CollectionService")
local replicatedStorage: ReplicatedStorage = game:GetService("ReplicatedStorage")
local userInputService: UserInputService = game:GetService("UserInputService")

local localPlayer: Player = game.Players.LocalPlayer
local playerGUI: PlayerGui = localPlayer:WaitForChild("PlayerGui")
local buildGUI: ScreenGui = playerGUI:WaitForChild("BuildGUI")
local mouse: Mouse = localPlayer:GetMouse()
local dragger: Dragger = Instance.new("Dragger")
local tool: Tool = script.Parent
local dragClone: Model? = nil
local rotationPointerPart: Part? = nil
local selectedItem: Model? = replicatedStorage.ItemModels.concrete:Clone()
local camera: Camera = workspace.CurrentCamera
local itemSelectedEvent: BindableEvent = buildGUI.BuildGUIScript.ItemSelectedEvent
local placeBlockFunction: RemoteFunction = replicatedStorage.Events.PlaceBlockFunction
local errorMessageEvent: BindableEvent = playerGUI.MiscGUI.ErrorMessageTextLabel.ErrorMessageScript.ErrorMessageEvent
local settingsFrame: Frame = buildGUI.ObjectSelectionFrame.SettingsFrame

local excludeList: {Instance} = {} 
local equipped: boolean = false
local _time: number = os.clock()
local unsnappedCFrame: CFrame = nil
local unsnappedRotation: CFrame = CFrame.Angles(math.rad(0), math.rad(0), math.rad(0))
local snappedRotation: CFrame = CFrame.Angles(math.rad(0), math.rad(0), math.rad(0))
local relativeRotation: boolean = true
local raycastResult: RaycastResult?
local moveSnap: number = 1
local rotateSnap: number = 1
local rotationModus: boolean = true

function destroyDragClone()
	if dragClone then
		dragClone:Destroy()
		dragClone = nil
	end
	if rotationPointerPart then
		rotationPointerPart:Destroy()
		rotationPointerPart = nil
	end
end

function createDragClone()
	if not selectedItem then
		return
	end

	if dragClone then
		destroyDragClone()
	end

	dragClone = selectedItem:Clone()
	dragClone.Parent = workspace
	for i, instance: Instance in pairs(dragClone:GetDescendants())  do
		if instance:IsA("BasePart") then
			instance.CanCollide = false
			instance.CanQuery = false
			instance.CanTouch = false
			if instance.Transparency <= 0.5 then
				instance.Transparency = 0.5
			end
		end
	end
	local dragPrimaryPart: BasePart = dragClone.PrimaryPart
	local rotationPointerClone: Part = game.ReplicatedFirst.RotationPointerPart:Clone()
	rotationPointerClone.Parent = workspace
	rotationPointerPart = rotationPointerClone
end

function roundNumberToBase(number: number, base: number, floor: boolean?): number
	return floor and math.floor(number / base) * base or math.round(number / base) * base
end

function roundVectorToBase(vector3: Vector3, base: number): Vector3
	return Vector3.new(
		roundNumberToBase(vector3.X, base),
		roundNumberToBase(vector3.Y, base),
		roundNumberToBase(vector3.Z, base)
	) 
end

function getPlacementCFrame(primaryPart: BasePart, rotationSnapped: boolean?): CFrame
	local rotationSnapped: boolean? = rotationSnapped
	if rotationSnapped == nil then
		rotationSnapped = true
	end

	assert(raycastResult, "raycastResult is nil!") 
	assert(primaryPart, "primaryPart is nil!") 
	
	local target: BasePart = raycastResult.Instance
	local hitPosition: Vector3 = raycastResult.Position
	
	local normal: Vector3 = raycastResult.Normal
	local normalCFrame: CFrame = CFrame.lookAt(hitPosition, hitPosition + normal)
	local surfaceNormalRelativeToTarget: Vector3 = target.CFrame:VectorToObjectSpace(raycastResult.Normal)

	local _rotation: CFrame = rotationSnapped and snappedRotation or unsnappedRotation
	if relativeRotation then
		local relativeVector: Vector3
		local absUpVector: Vector3 = Vector3.new(math.abs(target.CFrame.UpVector.X), math.abs(target.CFrame.UpVector.Y), math.abs(target.CFrame.UpVector.Z))
		local absRightVector: Vector3 = Vector3.new(math.abs(target.CFrame.RightVector.X), math.abs(target.CFrame.RightVector.Y), math.abs(target.CFrame.RightVector.Z))
		local absNormal: Vector3 = Vector3.new(math.abs(normal.X), math.abs(normal.Y), math.abs(normal.Z))
		if absUpVector:FuzzyEq(absNormal, 0.1) then
			relativeVector = target.CFrame.RightVector
			if absRightVector:FuzzyEq(absNormal, 0.1) then
				relativeVector = target.CFrame.LookVector
			end
		else
			relativeVector = target.CFrame.UpVector
		end

		local XVector: Vector3 = normal:Cross(relativeVector)
		local ZVector: Vector3 = XVector:Cross(normal)
		local _relativeRotation: CFrame = CFrame.fromMatrix(
			Vector3.new(),
			XVector,
			normal,
			ZVector
		)

		_rotation = _relativeRotation * _rotation 
	end
	
	if moveSnap ~= 0 then
		local hitPositionRelativeToTarget: CFrame = target.CFrame:ToObjectSpace(CFrame.lookAt(hitPosition, hitPosition - raycastResult.Normal))
		local surfaceNormalRelativeToTarget: Vector3 = target.CFrame:VectorToObjectSpace(normal)
		local hitPositionSnapped: CFrame = (hitPositionRelativeToTarget - hitPositionRelativeToTarget.Position) + roundVectorToBase(hitPositionRelativeToTarget.Position + surfaceNormalRelativeToTarget * 10, moveSnap)

		local snapRaycastOffsets: {Vector3} = {
			Vector3.new(0,0,0),
			hitPositionSnapped.RightVector * moveSnap,
			-hitPositionSnapped.RightVector * moveSnap,
			hitPositionSnapped.UpVector * moveSnap,
			-hitPositionSnapped.UpVector * moveSnap,
		}
		local snapRaycastResult: RaycastResult?
		for i = 1, 5 do
			local raycastParams: RaycastParams = RaycastParams.new()
			raycastParams.FilterDescendantsInstances = {target}
			raycastParams.FilterType = Enum.RaycastFilterType.Include
			
			local hitPositionSnappedCFrameWorldSpace: CFrame = target.CFrame:ToWorldSpace(hitPositionSnapped+snapRaycastOffsets[i])
			snapRaycastResult = workspace:Raycast(hitPositionSnappedCFrameWorldSpace.Position, normal * -1001, raycastParams)
			if snapRaycastResult then
				break
			end
		end

		if snapRaycastResult then
			hitPosition = snapRaycastResult.Position
		end
	end

	local resolvedCFrame: CFrame = CFrame.new(hitPosition) * _rotation
	
	local cornerPositionsRelativeToPrimaryPart: {Vector3} = {}
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(primaryPart.Size.X/2, primaryPart.Size.Y/2, primaryPart.Size.Z/2))
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(-primaryPart.Size.X/2, primaryPart.Size.Y/2, primaryPart.Size.Z/2))
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(primaryPart.Size.X/2, -primaryPart.Size.Y/2, primaryPart.Size.Z/2))
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(-primaryPart.Size.X/2, -primaryPart.Size.Y/2, primaryPart.Size.Z/2))
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(primaryPart.Size.X/2, primaryPart.Size.Y/2, -primaryPart.Size.Z/2))
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(-primaryPart.Size.X/2, primaryPart.Size.Y/2, -primaryPart.Size.Z/2))
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(primaryPart.Size.X/2, -primaryPart.Size.Y/2, -primaryPart.Size.Z/2))
	table.insert(cornerPositionsRelativeToPrimaryPart, Vector3.new(-primaryPart.Size.X/2, -primaryPart.Size.Y/2, -primaryPart.Size.Z/2))

	local cornerYPositionsRelativeToNormal: {number} = {}
	for i, cornerPosition in pairs(cornerPositionsRelativeToPrimaryPart) do
		local cornerPositionRelativeToNormal: Vector3 = normalCFrame:PointToObjectSpace(resolvedCFrame:PointToWorldSpace(cornerPosition))           
		table.insert(cornerYPositionsRelativeToNormal, cornerPositionRelativeToNormal.Z)
	end
	table.clear(cornerPositionsRelativeToPrimaryPart)

	local deepestCornerYPosition: number = 9999999999
	for i, cornerYposition in pairs(cornerYPositionsRelativeToNormal) do
		if cornerYposition < deepestCornerYPosition then
			deepestCornerYPosition = cornerYposition
		end
	end
		
	resolvedCFrame = CFrame.new(hitPosition + -deepestCornerYPosition * normal) * _rotation
	return resolvedCFrame
end

tool.Equipped:Connect(function()
	equipped = true
	createDragClone()
	buildGUI.Enabled = true
end)

tool.Unequipped:Connect(function()
	unsnappedRotation = CFrame.Angles(0, 0, 0)
	destroyDragClone()
	equipped = false
	buildGUI.Enabled = false
end)

mouse.Button1Down:Connect(function()
	_time = os.clock()
end)

mouse.Button1Up:Connect(function()
	--If the player held mouse button down for more than 0.25 secs then they probably want to cancel placing it
	if raycastResult and equipped and os.clock()-_time<0.25 and selectedItem and dragClone then
		local pressSound: Sound = tool.PressSound:Clone()
		pressSound.Parent = tool
		pressSound:Play()
		game:GetService("Debris"):AddItem(pressSound, pressSound.TimeLength)
		
		placeBlockFunction:InvokeServer(selectedItem.Name, getPlacementCFrame(dragClone.PrimaryPart), settingsFrame.MergeButton:GetAttribute("toggled"), settingsFrame.AnchoredButton:GetAttribute("toggled"), settingsFrame.WeldButton:GetAttribute("toggled"))
	end
end)

placeBlockFunction.OnClientInvoke = function(errorMessage: string)
	errorMessageEvent:Fire(errorMessage)
end

itemSelectedEvent.Event:Connect(function(itemName: string)
	selectedItem = replicatedStorage.ItemModels:FindFirstChild(itemName)
	createDragClone()
end)

userInputService.InputEnded:Connect(function(input: InputObject, gameProcessed: boolean)
	if gameProcessed then
		return
	end
	
	if input.UserInputType == Enum.UserInputType.Keyboard then
		local key: Enum.KeyCode = input.KeyCode
		if key == Enum.KeyCode.Q then
			rotationModus = not rotationModus
			if rotationModus then
				unsnappedRotation = snappedRotation
			end
		end
		if rotationModus then
			local rotateX: number = key == Enum.KeyCode.E and rotateSnap or 0
			local rotateY: number = key == Enum.KeyCode.R and rotateSnap or 0
			unsnappedRotation = unsnappedRotation * CFrame.Angles(math.rad(rotateX), math.rad(rotateY), 0)
			snappedRotation = unsnappedRotation
		end
	end
end)

while task.wait() do
	if equipped then
		local dragPrimaryPart: BasePart = dragClone and dragClone.PrimaryPart or nil

		moveSnap = tonumber(settingsFrame.SnapTextLabel.MoveSnapInput:GetAttribute("output"))
		rotateSnap = tonumber(settingsFrame.SnapTextLabel.RotateSnapInput:GetAttribute("output"))
		relativeRotation = settingsFrame.RelativeRotationButton:GetAttribute("toggled")

		local raycastParams: RaycastParams = RaycastParams.new()
		raycastParams.FilterDescendantsInstances = {dragClone}
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude

		table.clear(excludeList)
		table.insert(excludeList, test2)
		for i, placable: Model in pairs(collectionService:GetTagged("placable")) do
			for i, instance: any in pairs(placable:GetDescendants()) do
				if instance ~= placable.PrimaryPart then
					table.insert(excludeList, instance)
				end
			end
		end

		local unitRay: Ray = camera:ScreenPointToRay(mouse.X, mouse.Y)
		local orgin: Vector3 = unitRay.Origin
		raycastResult = workspace:Raycast(orgin, unitRay.Direction * 500, raycastParams)

		if not raycastResult then
			destroyDragClone()
		end
		if not dragClone and equipped and raycastResult then
			createDragClone()
			dragPrimaryPart = dragClone.PrimaryPart
		end
		if dragClone and raycastResult and mouse.Target then
			dragClone:PivotTo(getPlacementCFrame(dragPrimaryPart))
		end

		if rotationPointerPart and dragPrimaryPart then
			rotationPointerPart.CFrame = dragPrimaryPart.CFrame
			rotationPointerPart.Size = dragPrimaryPart.Size
		end
		if not rotationModus and raycastResult then
			local rotatingFactor: number = userInputService:IsKeyDown(Enum.KeyCode.LeftShift) and 0.05 or 2
			rotatingFactor = userInputService:IsKeyDown(Enum.KeyCode.LeftControl) and rotatingFactor * -1 or rotatingFactor
			local rotateX: number = userInputService:IsKeyDown(Enum.KeyCode.E) and 1 * rotatingFactor or 0
			local rotateY: number = userInputService:IsKeyDown(Enum.KeyCode.R) and 1 * rotatingFactor or 0
			unsnappedRotation = unsnappedRotation * CFrame.Angles(math.rad(rotateX), math.rad(rotateY), 0) 
			local rx, ry, rz = unsnappedRotation:ToOrientation();
			snappedRotation = CFrame.fromOrientation(math.rad(roundNumberToBase(math.deg(rx), rotateSnap, true)), math.rad(roundNumberToBase(math.deg(ry), rotateSnap, true)), math.rad(roundNumberToBase(math.deg(rz), rotateSnap, true)))

			if rotationPointerPart then
				rotationPointerPart.CFrame = getPlacementCFrame(dragPrimaryPart, false)
				rotationPointerPart.Size = dragPrimaryPart.Size
			end
		end
	end
end

there’s a lot of unrelated stuff, you might need to clean that out.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.