Snapping paintings to a wall

local function round(number)
	return math.floor((number/var.Grid) + 0.5)*var.Grid
end

if mouse.Target.Name == 'Wall' then
	local unitRay  = workspace.CurrentCamera:ScreenPointToRay(mouse.X, mouse.Y, 1)
	local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000)
	local hit, pos, normal = workspace:FindPartOnRay(ray, itemClone)
	itemClone:SetPrimaryPartCFrame(CFrame.new(round(pos.X), round(pos.Y), round(pos.Z))*CFrame.Angles(0, math.rad(90), var.Rotation))
end

End result
com-video-to-gif%20(1)
So the 2 problems are

  • It moves onto my character when I get close, even tho the mouse.Target is the wall
  • It isn’t attaching to the wall

It works great on the front facing walls, but not the side walls. Pretty sure it has something to do with the math.rad(90) on the Y axis of Angles.

2 Likes
  • It moves onto my character when I get close, even tho the mouse.Target is the wall

Yes the mouse target is the wall but the ray you cast from the camera hits your character before the wall. You might want to add the character to the ignore list.

  • It isn’t attaching to the wall

You answered this yourself, the rotation of the part you’re placing is static on the Y axis. You might want to base this on the surface normal instead.

2 Likes

How would I get the ray to ignore my player then?

And I figured that was the case for it not attaching to other walls, but I don’t know how to fix it. Getting rid of the math.rad(90) means it doesnt attach to any of the walls

Either with IgnoreList or WhiteList.

Well just getting rid of it means the static value is 0 instead of 90, no real difference. Instead you can get the surface normal from the raycast to base the rotation on, as I mentioned in my first comment (third returned value).

Where would I implement the surface normal into the CFrame??

itemClone:SetPrimaryPartCFrame(CFrame.new(round(pos.X), round(pos.Y), round(pos.Z))*normal*CFrame.Angles(0, 0, var.Rotation))

Doing this just made the model disappear :sweat_smile: probably somewhere millions of miles away in space

Well the normal is just the outwards direction of the side you hit (Vector3)

image

Perhaps a start would be
CFrame.new(pos, pos+normal)

I tried that before, but that screws with my rounding system

local function round(number)
	return math.floor((number/var.Grid) + 0.5)*var.Grid
end

itemClone:SetPrimaryPartCFrame(CFrame.new(round(pos.X), round(pos.Y), round(pos.Z))*CFrame.Angles(0, 0, var.Rotation))

Instead if I did this

itemClone:SetPrimaryPartCFrame(CFrame.new(pos, pos+normal)*CFrame.Angles(0, 0, var.Rotation))

then it wouldnt snap to a 1 stud grid

Then add rounding to the position…

pos = Vector3.new(round(pos.X), round(pos.Y), round(pos.Z))
CFrame.new(pos, pos+normal)



The painting just sits inside the wall now :sweat_smile:

Then you might want to offset it outwards, you’d have to place the primary part in each model to correctly line up when its CFrame is set.

Edit: Keep in mind this can be due to the fact that you are rounding. If you don’t it would keep the position at the raycast hit.

That would kinda make sense. However, without rounding it just moves freely completely. Surely there’s a way to round based on the side of the wall it’s facing

Sure, just not as straight forward. You’d need to define how you want it to snap since the previous definition is no longer enough. Ideas would be to find the closest point on a side to a certain grid position or only round the axis perpendicular to our “forward” and let the part/hit decide the parallel axis. Forward could for example be the axis the normal is closest to.

Decided to remove the rounding for now. It’s starting to look better now, however, it part still clips into the wall, meaning it can’t be placed at all

if mouse.Target.Name == 'Wall' then
	local unitRay  = workspace.CurrentCamera:ScreenPointToRay(mouse.X, mouse.Y, 1)
	local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000)
	local hit, pos, normal = workspace:FindPartOnRay(ray, itemClone)
	itemClone:SetPrimaryPartCFrame(CFrame.new(pos, pos + normal*15)*CFrame.Angles(0, 0, var.Rotation))
end
			
for _, v in pairs(getTouchingParts(itemClone.PrimaryPart)) do
	if not v:IsDescendantOf(itemClone) then
		itemClone.PrimaryPart.Transparency = 0.5	
		break			
	end
	itemClone.PrimaryPart.Transparency = 1
end

EDIT And still getting the problem with the ray on the player. Tried the Whitelist one but that just made the object disappear completely

This should do the trick:

local coords = {'X', 'Y', 'Z'}
local function getCFrame(mouse, thickness)
	if mouse.Target.Name == 'Wall' then
		local CF = mouse.Target.CFrame
		local touch = CF:Inverse() * mouse.Hit

		local pos = {}

		local normalAxis = mouse.TargetSurface % 3 + 1
		local dir = {0, 0, 0}
		for axis, coord in next, coords do
			if axis == normalAxis then
				pos[axis] = touch[coord] + thickness / 2
				dir[axis] = math.sign(touch[coord])
			else
				pos[axis] = math.floor(touch[coord] + 0.5)
			end
		end
		pos = Vector3(unpack(pos))
		dir = Vector3(unpack(dir))

		return CF * CFrame.new(pos, pos + normal)
	end
end

Since it uses mouse.Target, mouse.Hit, and mouse.TargetSurface instead of FindPartOnRay, it ignores the character by default. I also round on the position while in local space. Since to continue to be on the surface we need to keep one axis unaltered I only round two axis depending on the TargetSurface. I also added in the painting thickness so that it won’t partially go into the wall and return the correct CFrame for the painting. When the mouse isn’t pointing at a wall it returns nil.

Edit: Updated code to be a bit cleaner and update the normal correctly. I also noticed that I accidentally replied to Ninjo, sorry!

Keep in mind the hit of the raycast position is exactly on the parts side, and parts are positioned with CFrame based on their center, I suggest you edit each model to have a primary part that is allowed to clip into the wall so you can model the rest of the model based on that, knowing it will look appropriate on the wall, as I mentioned earlier.

The 15 in that snippet does nothing, are you trying to move the CFrame 15 studs?

Did you understand how to use it? If you just want to ignore the player try the ignore list instead, if that’s easier.

Edit: You could also round the wall coordinates by creating a new origin and snapping along the side of the part instead of the global axes.

So would it be

item:SetPrimaryPartCFrame(getCFrame(mouse, thickness))

Not entirely sure what thickness relates to??

local unitRay  = workspace.CurrentCamera:ScreenPointToRay(mouse.X, mouse.Y, 1)
local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000)
local hit, pos, normal = workspace:FindPartOnRay(ray, itemClone)
					
local coords = {'X', 'Y', 'Z'}
local CF = mouse.Target.CFrame
local touch = CF:Inverse() * mouse.Hit
			
local pos = {}
			
local normalAxis = mouse.TargetSurface % 3 + 1
local dir = {0, 0, 0}
for axis, coord in next, coords do
	if axis == normalAxis then
		pos[axis] = touch[coord] + thickness / 2
		dir[axis] = math.sign(touch[coord])
	else
		pos[axis] = math.floor(touch[coord] + 0.5)
	end
end

pos = Vector3(unpack(pos))
dir = Vector3(unpack(dir))
								
itemClone:SetPrimaryPartCFrame(CF * CFrame.new(pos, pos + normal))

Yup!

The thickness is the thickness of the painting. This also assumes that the front of the painting is the direction facing from the painting to the viewer. If not, then you may need to apply a rotation to the painting.

You also can get rid of the unitRay, ray, hit, pos, and normal variables you have at the top of the file. They are not used.

Also, in the return of your function, normal is never mentioned?

The normal isn’t returned because the CFrame is defined by both the position and the normal. It wraps them both into one neat package ready to be used ^.^

local coords = {'X', 'Y', 'Z'}

local function getCFrame(mouse, thickness)
	local CF = mouse.Target.CFrame
	local touch = CF:Inverse() * mouse.Hit
	
	local pos = {}

	local normalAxis = mouse.TargetSurface % 3 + 1 --ERROR HERE
	local dir = {0, 0, 0}
	for axis, coord in next, coords do
		if axis == normalAxis then
			pos[axis] = touch[coord] + thickness / 2
			dir[axis] = math.sign(touch[coord])
		else
			pos[axis] = math.floor(touch[coord] + 0.5)
		end
	end
	pos = Vector3(unpack(pos))
	dir = Vector3(unpack(dir))
	
	return CF * CFrame.new(pos, pos + normal)
end

-- Later down the script
if mouse.Target.Parent:IsDescendantOf(playersPlot.House) then -- Just checks to make sure the target is on the wall
	itemClone:SetPrimaryPartCFrame(getCFrame(mouse, 0.1))
end

Error

[attempt to perform arithmetic on field ‘TargetSurface’ (a userdata value)]