How to Get Size From Surface

I just spent a while and got this

local CRay = workspace:Raycast(workspace.RayCastInstance.Position,Vector3.new(-50,-0,0)) 
if CRay then
	local GCF = Instance.new("Part") --Get CFrame Part
	GCF.CFrame = CFrame.new(GCF.Position,GCF.Position+CRay.Normal)
	workspace.RayCastInstance.CFrame = 
        CFrame.fromMatrix(workspace.RayCastInstance.Position,GCF.CFrame.UpVector,CRay.Normal)
        wait()	
        GCF:Destroy()
end

Got it. I think this is what you are asking for, anyways. Basically, the plan is:

  1. Find two vectors that are orthogonal to each other, and parallel to the surface
  2. Find which one is closer to the world Y axis and choose that as the surface Y axis
  3. Compute the surface X axis based on that and the surface normal
  4. We have the CFrame of the surface now, we can calculate the size of the part in “surface space” by doing (size of part in object space) → to world → to surface

This gives the following result:

This code was placed in StarterCharacterScripts:

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local p = Instance.new("Part")
p.CanCollide = false
p.Anchored = true
p.Transparency = 0.5
p.BrickColor = BrickColor.Red()
local decal = Instance.new("Decal")
decal.Texture = "http://www.roblox.com/asset/?id=67616959" -- some random "this side up" image
decal.Face = Enum.NormalId.Back -- always the back face
decal.Parent = p
p.Parent = player.Character or player.CharacterAdded:Wait()

local function GetWorldOrientedSurface(part: BasePart, normalId: NormalId)
	local cf = part.CFrame
	local n = Vector3.fromNormalId(normalId) 
	local nWorld = cf:VectorToWorldSpace(n)

	-- pick arbitrary vector parallel to one of the edges
	local x = cf.LookVector
	if math.abs(x:Dot(nWorld)) > 0.9999 then
		x = cf.RightVector -- for front/back face
	end

	-- calculate another vector orthongal to the first on the surface
	local y = nWorld:Cross(x)

	-- flip up if pointing down
	x = math.sign(x.Y) * x
	y = math.sign(y.Y) * y

	-- choose which one is "more up" as the y axis
	y = y.Y > x.Y and y or x

	-- recalculate x axis based on that y axis vector
	x = y:Cross(nWorld)

	-- get position on surface
	local partSizeNormalAxisOnly = part.Size * n -- e.g. (3, 4, 5) * (0, 1, 0) -> (0, 4, 0)
	local partSizeInNormal = partSizeNormalAxisOnly.X + partSizeNormalAxisOnly.Y + partSizeNormalAxisOnly.Z -- e.g. (0, 4, 0) -> 4
	local surfacePos = part.Position + nWorld * math.abs(partSizeInNormal) / 2

	-- put together cframe matrix
	local surfaceCFrame = CFrame.fromMatrix(surfacePos, x, y, nWorld)

	-- get width of part in direction of x and y
	local sizeInWorldSpace = part.CFrame:VectorToWorldSpace(part.Size)
	local sizeInSurfaceSpace = surfaceCFrame:VectorToObjectSpace(sizeInWorldSpace)

	-- correct for negatives
	sizeInSurfaceSpace = Vector3.new(
		math.abs(sizeInSurfaceSpace.X), 
		math.abs(sizeInSurfaceSpace.Y), 
		math.abs(sizeInSurfaceSpace.Z))

	return surfaceCFrame, sizeInSurfaceSpace
end

game:GetService("RunService").Stepped:Connect(function()
	local part = mouse.Target
	if part then
		local cframe, size = GetWorldOrientedSurface(part, mouse.TargetSurface)
		-- size.Z is the depth of the part relative to the surface, but we don't care about that
		p.Size = Vector3.new(size.X, size.Y, 0.1)
		p.CFrame = cframe
	end
end)

Edit: more tests:

1 Like

It seems to fail on the top and bottom faces if they’re axis-aligned. Working on it :slight_smile:

1 Like

Thanks for the immense effort, but my brain is a bit fried and I’m having trouble implementing your function into my build script

local e = true
local qr
local tb
local mou = game.Players.LocalPlayer:GetMouse()
local function raysurfaceface(e)
	local dotY = e:Dot(Vector3.new(0,1,0))
	local dotX = e:Dot(Vector3.new(1,0,0))
	local dotZ = e:Dot(Vector3.new(0,0,1))
	local surface
	if (math.abs(dotY) >= 0.9) then
		if (dotY < 0) then
			surface = Enum.NormalId.Bottom
		else
			surface = Enum.NormalId.Top
		end
	elseif (math.abs(dotX) >= 0.9) then
		if (dotX < 0) then
			surface = Enum.NormalId.Left
		else
			surface = Enum.NormalId.Right
		end
	else
		if (dotZ < 0) then
			surface = Enum.NormalId.Front
		else
			surface = Enum.NormalId.Back
		end
	end
	return surface
end
local function vec3Func(f, ...)
	local x, y, z = {}, {}, {}
	for i, v in next, {...} do
		x[i], y[i], z[i] = v.x, v.y, v.z
	end
	return Vector3.new(f(unpack(x)), f(unpack(y)), f(unpack(z)))
end
local function getRotationBetween(u, v, axis)
	local dot, uxv = u:Dot(v), u:Cross(v)
	if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
	return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end
local function getSurfaceCF(part, lnormal)
	local pcf, size2 = part.CFrame, part.Size/2
	local transition = getRotationBetween(Vector3.new(0,0,1), lnormal, Vector3.new(0,1,0))
	local size = vec3Func(math.abs, transition:VectorToWorldSpace(part.Size))
	return (pcf * transition) * CFrame.new(size/2 * Vector3.new(0, 0, 1)), size
end
local function boundingbox(size,cf)
	local a = {
		Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
		Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2,
		Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
		Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2,
		Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
		Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2,
		Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
		Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2
	}
	local minlpx = 0
	local minlpy = 0
	local minlpz = 0
	local maxlpx = 0
	local maxlpy = 0
	local maxlpz = 0
	for i=1,#a do
		if a[i].X<minlpx then
			minlpx=a[i].X
		end
	end
	for i=1,#a do
		if a[i].Y<minlpy then
			minlpy=a[i].Y
		end
	end
	for i=1,#a do
		if a[i].Z<minlpz then
			minlpz=a[i].Z
		end
	end
	for i=1,#a do
		if a[i].X>maxlpx then
			maxlpx=a[i].X
		end
	end
	for i=1,#a do
		if a[i].Y>maxlpy then
			maxlpy=a[i].Y
		end
	end
	for i=1,#a do
		if a[i].Z>maxlpz then
			maxlpz=a[i].Z
		end
	end
	return Vector3.new(minlpx,minlpy,minlpz), Vector3.new(maxlpx,maxlpy,maxlpz)
end
local function getSurfaceSize(CRay)
	local List = {"UpVector","LookVector","RightVector"}
	local List2 = {}
	local Sides = {{"X","Y","Z"},{"X","Z","Y"},{"Y","X","Z"}}
	for count = 1,6,1 do
		local modi = -1+(count%2)*2
		List2[CRay.Instance.CFrame[List[math.ceil(count/2)]]*modi] = Sides[math.ceil(count/2)]
	end
	local Size = CRay.Instance.Size
	local N = List2[CRay.Normal]
	return Vector3.new(Size[N[1]],Size[N[2]],Size[N[3]])
end
local function GetWorldOrientedSurface(part, normalId)
	local cf = part.CFrame
	local n = Vector3.fromNormalId(normalId) 
	local nWorld = cf:VectorToWorldSpace(n)

	-- pick arbitrary vector parallel to one of the edges
	local x = cf.LookVector
	if math.abs(x:Dot(nWorld)) > 0.9999 then
		x = cf.RightVector -- for front/back face
	end

	-- calculate another vector orthongal to the first on the surface
	local y = nWorld:Cross(x)

	-- flip up if pointing down
	x = math.sign(x.Y) * x
	y = math.sign(y.Y) * y

	-- choose which one is "more up" as the y axis
	y = y.Y > x.Y and y or x

	-- recalculate x axis based on that y axis vector
	x = y:Cross(nWorld)

	-- get position on surface
	local partSizeNormalAxisOnly = part.Size * n -- e.g. (3, 4, 5) * (0, 1, 0) -> (0, 4, 0)
	local partSizeInNormal = partSizeNormalAxisOnly.X + partSizeNormalAxisOnly.Y + partSizeNormalAxisOnly.Z -- e.g. (0, 4, 0) -> 4
	local surfacePos = part.Position + nWorld * math.abs(partSizeInNormal) / 2

	-- put together cframe matrix
	local surfaceCFrame = CFrame.fromMatrix(surfacePos, x, y, nWorld)

	-- get width of part in direction of x and y
	local sizeInWorldSpace = part.CFrame:VectorToWorldSpace(part.Size)
	local sizeInSurfaceSpace = surfaceCFrame:VectorToObjectSpace(sizeInWorldSpace)

	-- correct for negatives
	sizeInSurfaceSpace = Vector3.new(
		math.abs(sizeInSurfaceSpace.X), 
		math.abs(sizeInSurfaceSpace.Y), 
		math.abs(sizeInSurfaceSpace.Z))

	return surfaceCFrame
end
local tb = Instance.new("Part",workspace)
tb.Anchored=true


game:GetService("RunService").RenderStepped:Connect(function()
	if e==true then
		local rcp = RaycastParams.new()
		rcp.FilterType=Enum.RaycastFilterType.Blacklist
		rcp.FilterDescendantsInstances={game.Players.LocalPlayer.Character,tb}
		local ray = workspace:Raycast(workspace.CurrentCamera.CFrame.Position,(mou.Hit.p-workspace.CurrentCamera.CFrame.Position).Unit*500,rcp)
		if ray==nil then
			tb.Parent=nil
		else
			local f1 = 0
			local f2
			print(ray.Normal.Unit)
			if math.abs(ray.Normal.Unit.X)>f1 then
				f1=math.abs(ray.Normal.Unit.X)
				f2 = "x"
			end
			if math.abs(ray.Normal.Unit.Y)>f1 then
				f1=math.abs(ray.Normal.Unit.Y)
				f2 = "y"
			end
			if math.abs(ray.Normal.Unit.Z)>f1 then
				f1=math.abs(ray.Normal.Unit.Z)
				f2 = "z"
			end
			local r = 45
			local jarsx,jarsy,jarsz
			if f2=="y" then
				jarsx,jarsy,jarsz = ((CFrame.lookAt(Vector3.new(0,0,0),ray.Normal.Unit,ray.Instance.CFrame:VectorToWorldSpace(Vector3.new(0,1,0)))*CFrame.fromOrientation(math.rad(-90),0,0))*CFrame.fromOrientation(0,math.rad(r),0)):ToOrientation()
			else
				jarsx,jarsy,jarsz = ((CFrame.lookAt(Vector3.new(0,0,0),ray.Normal.Unit,Vector3.new(0,1,0))*CFrame.fromOrientation(math.rad(-90),0,0))*CFrame.fromOrientation(0,math.rad(r),0)):ToOrientation()
			end
			tb.Parent=workspace
			local i1 = Vector3.new(ray.Position.X,ray.Position.Y,ray.Position.Z)
			--if raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Bottom then
			--	i1=i1-(tb.PrimaryPart.CFrame.UpVector.Unit*(tb.PrimaryPart.Size.Y/2))
			--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Top then
			--	i1=i1+(tb.PrimaryPart.CFrame.UpVector.Unit*(tb.PrimaryPart.Size.Y/2))
			--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Front then
			--	i1=i1+(tb.PrimaryPart.CFrame.LookVector.Unit*(tb.PrimaryPart.Size.Z/2))
			--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Back then
			--	i1=i1-(tb.PrimaryPart.CFrame.LookVector.Unit*(tb.PrimaryPart.Size.Z/2))
			--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Right then
			--	i1=i1+(tb.PrimaryPart.CFrame.RightVector.Unit*(tb.PrimaryPart.Size.X/2))
			--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Left then
			--	i1=i1-(tb.PrimaryPart.CFrame.RightVector.Unit*(tb.PrimaryPart.Size.X/2))
			--end
			local intended = CFrame.new(i1)*CFrame.fromOrientation(jarsx,jarsy,jarsz)
			local p1,p2 = boundingbox(tb.Size,CFrame.fromOrientation(jarsx,jarsy,jarsz))
			local intended2
			print(f2)
			print(getSurfaceSize(ray))
			local kars = GetWorldOrientedSurface(ray.Instance,raysurfaceface(ray.Normal))
			if f2=="y" then
				print(math.clamp(kars:pointToObjectSpace(intended.Position).X,-getSurfaceSize(ray).X/2+math.abs(p1.X-p2.X)/2,getSurfaceSize(ray).X/2-math.abs(p1.X-p2.X)/2))
				print(kars:pointToObjectSpace(intended.Position).Y)
				print(math.clamp(kars:pointToObjectSpace(intended.Position).Z,-getSurfaceSize(ray).Z/2+math.abs(p1.Z-p2.Z)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Z-p2.Z)/2))
				intended2 = CFrame.new(Vector3.new(math.clamp(kars:pointToObjectSpace(intended.Position).X,-getSurfaceSize(ray).X/2+math.abs(p1.X-p2.X)/2,getSurfaceSize(ray).X/2-math.abs(p1.X-p2.X)/2),kars:pointToObjectSpace(intended.Position).Y,math.clamp(kars:pointToObjectSpace(intended.Position).Z,-getSurfaceSize(ray).Z/2+math.abs(p1.Z-p2.Z)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Z-p2.Z)/2)))
			elseif f2=="x" then
				intended2 = CFrame.new(Vector3.new(kars:pointToObjectSpace(intended.Position).X,math.clamp(kars:pointToObjectSpace(intended.Position).Y,-getSurfaceSize(ray).Z/2+math.abs(p1.Y-p2.Y)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Y-p2.Y)/2),math.clamp(kars:pointToObjectSpace(intended.Position).Z,-getSurfaceSize(ray).Z/2+math.abs(p1.Z-p2.Z)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Z-p2.Z)/2)))
			elseif f2=="z" then
				intended2 = CFrame.new(Vector3.new(math.clamp(kars:pointToObjectSpace(intended.Position).X,-getSurfaceSize(ray).X/2+math.abs(p1.X-p2.X)/2,getSurfaceSize(ray).X/2-math.abs(p1.X-p2.X)/2),math.clamp(kars:pointToObjectSpace(intended.Position).Y,-getSurfaceSize(ray).Z/2+math.abs(p1.Y-p2.Y)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Y-p2.Y)/2),kars:pointToObjectSpace(intended.Position).Z))
			end
			intended2=CFrame.new(kars:pointToWorldSpace(intended2.Position))
			intended2=CFrame.new(intended2.Position)*CFrame.fromOrientation(jarsx,jarsy,jarsz)
			game:GetService("TweenService"):Create(tb,TweenInfo.new(.1/3,Enum.EasingStyle.Quad,Enum.EasingDirection.InOut),{CFrame=intended2}):Play()
		end
	end
end)

Place it in StarterGui

I’m not sure what this script is supposed to be doing, but

  1. raysurfaceface looks like it takes an object-space normal not a world-space normal (which you’re currently doing) so the call to GetWorldOrientedSurface should look like
GetWorldOrientedSurface(
  ray.Instance,
  raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal)))
  1. GetWorldOrientedSurface returns two things:

    a) A CFrame representing the center of the surface, aligned with the surface, and
    b) A Vector3 representing the size of the part (relative to the surface cframe). The X and Y components of this would be the actual dimensions of the surface.

I don’t quite understand what you’re trying to do with the kars variable and everything, but maybe this helps clear things up.

I don’t quite understand what you’re trying to do with the kars variable and everything, but maybe this helps clear things up.

Yeah, because I’m using the kars variable for these very long lines of code

It’s supposed to be a building script, and right now I’m trying to work on finding the boundaries/clamps for the part on surfaces

GetWorldOrientedSurface(
  ray.Instance,
  raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal)))

I did this already, I know how to call the function and stuff, but as you can see I don’t know what to do with its result. I’m needing this for finding the correct world axis orientated CFrame for using :pointToObjectSpace on, so that in an example, instead of (25,10,1) for the position of the part by :pointToObjectSpace’d on, then it’s supposed to be (25,1,10) if it’s on a flat surface that’s facing up but the part itself is rotated (90,0,0), thus the surface facing up is its front surface.

local function raysurfaceface(e)
	local dotY = e:Dot(Vector3.new(0,1,0))
	local dotX = e:Dot(Vector3.new(1,0,0))
	local dotZ = e:Dot(Vector3.new(0,0,1))
	local surface
	if (math.abs(dotY) >= 0.9) then
		if (dotY < 0) then
			surface = Enum.NormalId.Bottom
		else
			surface = Enum.NormalId.Top
		end
	elseif (math.abs(dotX) >= 0.9) then
		if (dotX < 0) then
			surface = Enum.NormalId.Left
		else
			surface = Enum.NormalId.Right
		end
	else
		if (dotZ < 0) then
			surface = Enum.NormalId.Front
		else
			surface = Enum.NormalId.Back
		end
	end
	return surface
end

Returns the surface enum for the surface normal of the raycast

Yes, I read that function. I’m saying that the way raysurfaceface is written, it assumes e is an object-space normal vector.

However, you’re calling it like this:

This is wrong because ray.Normal is a world-space normal. I’m suggesting you fix the call site to be

So you’re passing it an object-space normal instead.

I don’t really want to look through all this code to figure out what you’re trying to do right now, but I can explain more about what GetWorldOrientedSurface returns, if that would be useful.

So I’m having this very weird output here


image
Corresponds to

local kars = GetWorldOrientedSurface(ray.Instance,raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal)))
print(kars.LookVector)
print(kars.UpVector)
print(ray.Normal)

I don’t know why the kars lookvector is faced downwards, and also when I print
kars:pointToObjectSpace(intended.Position)
It still returns the usual (x,y,0) instead of (x,0,y) which I was hoping to achieve by finding the world oriented cframe.

And you will see, the reason why I want (x,0,y) is because on a normal top facing surface with rotation (0,0,0), what would matter is the X and Z coordinates, so it would be (clamp(x),y,clamp(z)), but in a top facing surface with rotation (90,0,0), where the top facing surface is actually the part’s front surface, when CFrame:PointToObjectSpace is performed for the ray’s hit coordinates, it would return (x,y,0), instead of (x,0,y), so that’s why the Y coordinate (Z coordinate when using CFrame:PointToWorldSpace) isn’t clamped.

1 Like

It’s hard to explain, but using rot(90,0,0) size(39,54,2) — kars:PointToObjectSpace() makes the (X,Y,Z) coordinates of the ray hit position into (X,Z,Y), and using rot(90,0,0) size(39,54,2) — kars:PointToWorldSpace() for the converted coordinates will return (X,Z,Y) into (X,Y,Z), and the reason this is problematic is because I’m only clamping the X and Z coordinates for Y facing surfaces, so Y ends up not being clamped in a situation like this, so it becomes (clamp(X),clamp(Y),z) when converted back.

I’ve also drawn a diagram if my words are getting harder to understand

I’ll look at it a little more but just so you’re aware I wrote that function so that the CFrame returned would have its right and upvectors aligned to the surface, while the surface normal is the -LookVector. So it’s expected at least that the look vector would be pointing into the surface

Have you figured out the answer? It’s been an hour already

Have you figured out the solution to this problem

It seems to fail on the top and bottom faces if they’re axis-aligned. Working on it :slight_smile:

As well as this problem?

I’ve also drawn a diagram if my words are getting harder to understand

Hey man, I’m volunteering my time like everyone else on this forum.

Anyways, yes.

This version of the function returns a cframe where the RightVector and -LookVector are parallel to the surface, and the UpVector is pointing out from the surface. The second returned value is the size of the surface in a Vector2 this time for clarity.

To be clear: the CFrame this returns has a positive X-axis which points “to the right”, a positive Z-axis which points “down”, and a positive Y-axis which points “out of the page”.

However, the size returned is a Vector2, which means that it’s X-axis is aligned with the CFrame’s X-axis, but the size-X-axis is aligned with the cframe-Z-axis.

Visualization of the CFrame:

-- Returns a CFrame and a Size (Vector2) of the given surface.
--
-- The CFrame:
--   * is positioned at the center of the surface
--   * has an UpVector = the surface normal
--   * has a LookVector aligned with a part edge, and pointing generally upwards in world space.
local function GetWorldOrientedSurface(part: BasePart, normalId: Enum.NormalId): (CFrame, Vector2)
	local cf = part.CFrame
	local rot = cf - cf.Position
	
	local nObject = Vector3.fromNormalId(normalId)
	local nWorld = rot * nObject
	
	-- get orthogonal vector by utilizing the order of NormalId enums
	-- i.e. Front.Value is 5 -> (5+1)%6 = 0 -> Right.Value
	local xWorld = rot * Vector3.fromNormalId((normalId.Value + 1) % 6)
	
	-- get other orthogonal vector
	local zWorld = nWorld:Cross(xWorld)
	
	-- make them both point "generally down"
	if xWorld.Y > 0 then xWorld = -xWorld end
	if zWorld.Y > 0 then zWorld = -zWorld end
	
	-- choose the one pointing "more down" one as the z axis for the surface
	if xWorld.Y < zWorld.Y then zWorld = xWorld end
	
	-- redefine x axis based on that
	xWorld = nWorld:Cross(zWorld)
	
	local surfaceRot = CFrame.fromMatrix(Vector3.new(), xWorld, nWorld, zWorld)

	-- get width of part in direction of x and y
	local sizeInWorldSpace = rot * part.Size
	local sizeInSurfaceSpace = surfaceRot:Inverse() * sizeInWorldSpace
	
	-- get position on surface
	local surfaceCFrame = surfaceRot + cf.Position + nWorld * math.abs(sizeInSurfaceSpace.Y) / 2

	return surfaceCFrame, Vector2.new(math.abs(sizeInSurfaceSpace.X), math.abs(sizeInSurfaceSpace.Z))
end

-- USAGE

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local p = Instance.new("Part")
p.CanCollide = false
p.Anchored = true
p.Transparency = 0.5
p.BrickColor = BrickColor.Red()
local decal = Instance.new("Decal")
decal.Texture = "http://www.roblox.com/asset/?id=67616959" -- some random "this side up" image
decal.Face = Enum.NormalId.Top -- always the top face
decal.Parent = p
p.Parent = player.Character or player.CharacterAdded:Wait()

game:GetService("RunService").Stepped:Connect(function()
	local part = mouse.Target
	if part then
		local cframe, size = GetWorldOrientedSurface(part, mouse.TargetSurface)
		p.Size = Vector3.new(size.X, 0.1, size.Y)
		p.CFrame = cframe
	end
end)
6 Likes

:pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley::pray::angel::grinning_face_with_smiling_eyes::smiling_face_with_three_hearts::smiley:

Thank you so much for this, now time to clean up my code :angel: