Snapping object to grid relative to an angled part

Hello!

Currently working on a placement system as a means to practice my scripting skills, however I have run into a problem. When putting my mouse over a wall, the object should snap to the wall’s rotation and move along the wall depending on the mouse position. The problem is that I am not sure how I would go about doing this because I don’t know how to assign the part’s position relative to the centre of the wall if that makes sense.

I have looked up things like Object and World space, however I can’t really seem to understand how it works and how I would go about solving this issue.

Object placement script, it snaps it to the wall but I’m not able to move the part along the wall if that makes sense:

local item
local snappedToWall = false
	currentConnections[#currentConnections + 1] = runService.RenderStepped:Connect(function()
		item = item or Instance.new('Part')
		item.Anchored = true
		local target = mouse.Target
		item.Parent = ignoreParts
		item.Position = snap(mouse.Hit.p) -- snaps the object to the grid, not the issue but I can post it should it be needed.
		if string.find(string.lower(target.Name), 'wall') then
			print('wall')
			item.CFrame = target.CFrame * CFrame.new(item.Size.X / 2, 0, 0) -- offsets the part from the wall's CFrame
		end
	end)

Video of the problem and how far I’ve gotten:

EDIT:

I have come up with somewhat of a solution, but I still need help if anyone can help with that.

2 Likes

Hey, what’s in that snap() function…?

2 Likes

Just snaps it to the closest increment.

local gridSnap = 5

local function snap(vector)
	local newVector = Vector3.new(
		math.floor((vector.X / gridSnap)) * gridSnap,
		vector.Y,
		math.floor((vector.Z / gridSnap)) * gridSnap
	)

	return newVector
end
1 Like

Have you tried doing the snap function but do the same thing for Y as you do X and Z? (So like make a separate snapWall() function.

1 Like

The Y axis isn’t the problem. The Y axis being ignored just makes the object freely moveable along the Y axis.

1 Like

Yes, but you didn’t use the snap() function for walls…

1 Like

Right, it should snap along the width of the wall which is what I don’t know how to do.

1 Like

You have to somehow combine target.CFrame * CFrame.new(item.Size.X / 2, 0, 0) with math.floor((vector.X / gridSnap)) * gridSnap somehow…

Yeah, I’m stumped too. I’ll take another look at this tmr and try to figure it out.

Let me know if/when you do!

2 Likes

I did come up with somewhat of a solution but it comes back to snapping it to a grid which I still am having problems with.

What I have so far:

item.Position = mouse.Hit.p -- Snap it to the hit position
item.Orientation = target.Orientation -- Same orientation as the wall
item.CFrame *= CFrame.new((item.Size.X) / 2, 0, 0) -- offset it by half of the item's size

I also tried using modulus to achieve the grid snapping effect but it ended up being buggy and not really what I was looking for.

local function snapAxis(number)
    return math.floor((number / gridSnap) + 0.5) * gridSnap
end
			local yAxis = snapAxis(item.Position.Y)
			item.Position = Vector3.new(item.Position.X, yAxis, item.Position.Z)
			local posX, posZ
			if item.Position.X < 0 then
				posX = -item.Position.X
			else
				posX = item.Position.X
			end
			if item.Position.Z < 0 then
				posZ = -item.Position.Z
			else
				posZ = item.Position.Z
			end
			local diffX = posX % gridSnap
			local diffZ = posZ % gridSnap
			item.CFrame -= Vector3.new(-diffX, 0, -diffZ) -- also tried multiplication

FINALLY got it!!! I was overcomplicating things way more than I had to.

local offsetCFrame = CFrame.new(snap(item.CFrame:ToObjectSpace(target.CFrame).Position)) -- find the difference between the hit position and the grid snap position relative to the mouse target
item.CFrame = target.CFrame:ToWorldSpace(CFrame.new(-offsetCFrame.Position.X, 0, -offsetCFrame.Position.Z)) -- set the new snapped position (ignore Y for now)
item.CFrame *= CFrame.new(item.Size.X / 2, 0, 0) -- offset for half of the item's size
item.Position = Vector3.new(item.Position.X, snapAxis(mouse.Hit.p.Y), item.Position.Z)  -- set the final position, snap to the Y axis
5 Likes

I’m just curious, what does the snap function?

It basically just rounds a number up or down.

1 Like

Hey there, I’m working on doing something similar, and I don’t quite understand your formula you’ve got there. I’m using things like :PivotTo(), and :TranslateBy() since my objects are models.

I was doing some math, and found that the magic number to make a part snap to the grid of the grid of the angled part is 0.707. (Edit: this only applies to objects at a rotation of 45 degrees)

Think you could help? I’m probably overthinking things here.

Hey, yeah I’m happy to, but I will give you an updated answer and proper explanation as I’ve learned a bunch since then. It was also an odd decision to use CFrame and Position separately. My examples will be supposing a part, but I will add the alternatives for using a model.

Now ideally you’d be raycasting from the mouse instead of using mouse.Target as the former will give us more predictability and flexibility.

It’s pretty simple to do this, camera is workspace.CurrentCamera:

local mouseLocation = userInputService:GetMouseLocation()

local ray = camera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)

local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances = { -- exclude our character (this will need to be added every time the character is added but I'm only doing this as an example)
	game:GetService('Players').LocalPlayer.Character;
}

-- this ray is what we will be using to replace the Mouse object:
local ray = workspace:Raycast(ray.Origin, ray.Direction * 1000, rayParams)

Anyway, first thing we would need to do is find where our mouse intersects the mouse target, get the normal (this faces away from where our mouse intersects the target), and the instance from which our rotation will be derived. These are simply ray.Position, ray.Normal, and ray.Instance.

local ray = workspace:Raycast(ray.Origin, ray.Direction * 1000, rayParams)

if not ray then
	return
end

local position = ray.Position
local normal = ray.Normal
local instance = ray.Instance

Now, we can move our our part or model to this position, and apply the rotation of ray.Instance (you can replace part.CFrame with model:PivotTo() here):

part.CFrame = CFrame.new(position) * instance.CFrame.Rotation

This will give us the following:
image

However as I’m sure you’ve noticed, the part is half inside instance, this is where ray.Normal comes into play. We can multiply part.Size * 0.5 by normal relative to instance.CFrame to get the offset to make the part properly aligned (depending on your requirements, part.Size can be replaced with model:GetExtentsSize() or model.PrimaryPart.Size)

local normalRelative = instance.CFrame:VectorToObjectSpace(normal)

part.CFrame = CFrame.new(position) * instance.CFrame.Rotation * CFrame.new(0.5 * part.Size * normalRelative)

Now all we have left to do is apply snapping logic. For this, we can take mouse.Position, make it relative to instance.Position, round the relevant axes, and then translate it back to world space.\

Note: depending on which face is currently being hovered over, you may not want to snap a specific axis, but this does not handle that for you. If you need help with an implementation, let me know.

local snappedPosition = Vector3.new(
	math.round(relativePosition.X / INCREMENT) * INCREMENT,
	math.round(relativePosition.Y / INCREMENT) * INCREMENT,
	math.round(relativePosition.Z / INCREMENT) * INCREMENT
)

local snappedPositionWorld = instance.CFrame:PointToWorldSpace(snappedPosition)

part.CFrame = CFrame.new(snappedPositionWorld) * instance.CFrame.Rotation * CFrame.new(0.5 * part.Size * normalRelative)

With that we end up with the following:
Medal_GGDRtplrIo

Full code
local userInputService = game:GetService('UserInputService')
local camera = workspace.CurrentCamera

local part = Instance.new('Part')
part.Anchored = true
part.Parent = workspace

local INCREMENT = 0.5

game:GetService('RunService').RenderStepped:Connect(function()
	local mouseLocation = userInputService:GetMouseLocation()

	local ray = camera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)

	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = { -- exclude our character (this will need to be added every time the character is added)
		game:GetService('Players').LocalPlayer.Character;
		part;
	}

	local ray = workspace:Raycast(ray.Origin, ray.Direction * 1000, rayParams)

	if not ray then
		return
	end

	local position = ray.Position
	local normal = ray.Normal
	local instance = ray.Instance
	
	local normalRelative = instance.CFrame:VectorToObjectSpace(normal)
	
	local relativePosition = instance.CFrame:PointToObjectSpace(position)

	local snappedPosition = Vector3.new(
		math.round(relativePosition.X / INCREMENT) * INCREMENT,
		math.round(relativePosition.Y / INCREMENT) * INCREMENT,
		math.round(relativePosition.Z / INCREMENT) * INCREMENT
	)

	local snappedPositionWorld = instance.CFrame:PointToWorldSpace(snappedPosition)

	part.CFrame = CFrame.new(snappedPositionWorld) * instance.CFrame.Rotation * CFrame.new(0.5 * part.Size * normalRelative)
end)
1 Like

Amazing! Thanks for getting back to me, I’ve been doing a crazy hacky method, so I’m hoping to use a formula that works for models since I’m trying to do something like place paintings (with multiple parts) on walls.

I heard you can just weld the parts, but when I did any CFraming with the PrimaryPart it would change the CFrame for ONLY the PrimaryPart, which is obviously not ideal.

On top of that, I also tried this ray method, but it was acting unreliably where it would sometimes get the raycast result information, and other times not. I filtered both the player and the object I’m intending to place onto the wall. So I’m kind of at a loss here.

Just to give you a glimpse of what I compromised on doing as an alternative. . .

					if MouseTarget:HasTag("AngledWall") then
						
						-- ## Can change the MouseTarget.Position to MousePos to allow sliding along the wall. But it doesn't snap ##--
						if MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) >= 45 and math.deg(wallY) <= 46 or MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) >= 44 and math.deg(wallY) <= 45 or
							MousePos.X < 0 and MousePos.Z < 0  and math.deg(wallY) >= 45 and math.deg(wallY) <= 46 or MousePos.X < 0 and MousePos.Z < 0 and math.deg(wallY) >= 44 and math.deg(wallY) <= 45 or 
							 MousePos.X > 0 and MousePos.Z < 0  and math.deg(wallY) >= 45 and math.deg(wallY) <= 46 or MousePos.X > 0 and MousePos.Z < 0 and math.deg(wallY) >= 44 and math.deg(wallY) <= 45 or 
							  MousePos.X < 0 and MousePos.Z > 0  and math.deg(wallY) >= 45 and math.deg(wallY) <= 46 or MousePos.X < 0 and MousePos.Z > 0 and math.deg(wallY) >= 44 and math.deg(wallY) <= 45 then

							GhostObject:PivotTo(CFrame.Angles(0, wallY, 0))
							GhostObject:TranslateBy(Vector3.new(MouseTarget.Position.X - 0.707, MouseTarget.Position.Y, MouseTarget.Position.Z - 0.707))
							surfaceValid = true

						elseif MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) <= -135 and math.deg(wallY) >= -136 or MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) >= -135 and math.deg(wallY) <= -134 or 
							MousePos.X < 0 and MousePos.Z < 0 and math.deg(wallY) <= -135 and math.deg(wallY) >= -136 or MousePos.X < 0 and MousePos.Z < 0 and math.deg(wallY) >= -135 and math.deg(wallY) <= -134 or 
							 MousePos.X > 0 and MousePos.Z < 0 and math.deg(wallY) <= -135 and math.deg(wallY) >= -136 or MousePos.X > 0 and MousePos.Z < 0 and math.deg(wallY) >= -135 and math.deg(wallY) <= -134 or 
							  MousePos.X < 0 and MousePos.Z > 0 and math.deg(wallY) <= -135 and math.deg(wallY) >= -136 or MousePos.X < 0 and MousePos.Z > 0 and math.deg(wallY) >= -135 and math.deg(wallY) <= -134 then

							GhostObject:PivotTo(CFrame.Angles(0, wallY, 0))
							GhostObject:TranslateBy(Vector3.new(MouseTarget.Position.X + 0.707, MouseTarget.Position.Y, MouseTarget.Position.Z + 0.707))
							surfaceValid = true

						elseif MousePos.X < 0 and MousePos.Z < 0 and math.deg(wallY) >= -45 and math.deg(wallY) <= -44 or MousePos.X < 0 and MousePos.Z < 0 and math.deg(wallY) <= -45 and math.deg(wallY) >= -46 or 
							MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) >= -45 and math.deg(wallY) <= -44 or MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) <= -45 and math.deg(wallY) >= -46 or 
							 MousePos.X > 0 and MousePos.Z < 0 and math.deg(wallY) >= -45 and math.deg(wallY) <= -44 or MousePos.X > 0 and MousePos.Z < 0 and math.deg(wallY) <= -45 and math.deg(wallY) >= -46 or 
							  MousePos.X < 0 and MousePos.Z > 0 and math.deg(wallY) >= -45 and math.deg(wallY) <= -44 or MousePos.X < 0 and MousePos.Z > 0 and math.deg(wallY) <= -45 and math.deg(wallY) >= -46 then

							GhostObject:PivotTo(CFrame.Angles(0, wallY, 0))
							GhostObject:TranslateBy(Vector3.new(MouseTarget.Position.X + 0.707, MouseTarget.Position.Y, MouseTarget.Position.Z - 0.707))
							surfaceValid = true

						elseif MousePos.X < 0 and MousePos.Z < 0 and math.deg(wallY) >= 135 and math.deg(wallY) <= 136 or MousePos.X < 0 and MousePos.Z < 0 and math.deg(wallY) <= 135 and math.deg(wallY) >= 134 or 
							MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) >= 135 and math.deg(wallY) <= 136 or MousePos.X > 0 and MousePos.Z > 0 and math.deg(wallY) <= 135 and math.deg(wallY) >= 134 or 
							 MousePos.X > 0 and MousePos.Z < 0 and math.deg(wallY) >= 135 and math.deg(wallY) <= 136 or MousePos.X > 0 and MousePos.Z < 0 and math.deg(wallY) <= 135 and math.deg(wallY) >= 134 or 
							  MousePos.X < 0 and MousePos.Z > 0 and math.deg(wallY) >= 135 and math.deg(wallY) <= 136 or MousePos.X < 0 and MousePos.Z > 0 and math.deg(wallY) <= 135 and math.deg(wallY) >= 134 then

							GhostObject:PivotTo(CFrame.Angles(0, wallY, 0))
							GhostObject:TranslateBy(Vector3.new((MouseTarget.Position.X - 0.707), MouseTarget.Position.Y, (MouseTarget.Position.Z + 0.707)))
							surfaceValid = true

						end

					end
					
				end

Yes you could do this, but you’d have to make sure every part except the primary part is unanchored. That being said, it’s much easier to just use PivotTo so you don’t have to deal with welds and unanchoring parts, and it doesn’t drift over time like SetPrimaryPartCFrame does.

Could you share the snippet where you’re creating your raycast params and adding the GhostObject and character to it? As long as its FilterType is Exclude, it should be ignoring whatever is in the FilterDescendantsInstances. However, you do say that you’re adding the player to the filter, it would have to be the character to be excluded.

1 Like

I had already long deleted that segment of code. I copied and pasted what you provided me in another place, and found that it works exactly as I would like mine to work. I’ll need to look at how to implement the math to the :TranslateBy() function or whatever other works best in this case for models.

Currently, I’m using the method I’ve shown above. I’ve tried fiddling around to see if I could somehow get it to use that magic number (0.707 for angles of 45 degrees) and adding that to the mouse position X and Z, but it would dramatically offset the object. I only intend to do angles of 45 degrees so it’s not much that I’d have to do.

I think you’d just have to do what I did here:

CFrame.new(snappedPositionWorld) * instance.CFrame.Rotation * CFrame.new(0.5 * part.Size * normalRelative)

0.707 is the direction of a 45 degree unit vector, so in this case, that’s what normalRelative represents.

1 Like

I’ll give that a shot, thanks!

1 Like