So there’s quite a lot to this question, but it’s a very good one imo. There are a number of problems we’ll have to solve here which I’ll outline below.
Keep in mind in all of this I am assuming we’re using brick shaped parts. If you’re not you can’t easily calculate surface size and thus clamping becomes difficult. In those cases I recommend ignoring clamping.
Getting a surface’s orientation from a surface normal.
I’ll leave this section relatively bare because I’ve discussed this before. See this thread for more information about why this function works and does what it does.
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(UNIT_Z, lnormal, UNIT_Y)
local size = vec3Func(math.abs, transition:VectorToWorldSpace(part.Size))
return (pcf * transition) * CFrame.new(size/2 * Vector3.new(0, 0, 1)), size
end
local target = mouse.Target
local normal = -- the normal of the surface your mouse is over
local rotation, surfaceSize = getSurfaceCF(target, target.CFrame:VectorToObjectSpace(normal))
Clamping to the surface for any orientation of our portal.
Now once you have the surface CFrame and surface Size it’s quite simple to clamp an object to the surface given you know the size of the part relative to its rotation. So if you don’t apply any extra rotation to the portal object you’d just clamp based on portal.Size. However, since we want the portal to rotate dependent on how are camera is looking at the surface we’ll need to be able to handle an arbitrary rotation.
We do this by finding that arbitrary rotation relative to the surface CFrame and then finding the bounding box size of object we’re placing. We can then clamp with this size value instead.
local corners = getCornersFromCFrameSize(surfCF, portal.Size) -- function gets corners from cframe and size
local max = vec3Func(math.max, unpack(corners))
local min = vec3Func(math.min, unpack(corners))
local pSize = max - min -- this is our world bounds size
We could then handle our clamping logic:
if (pSize.x > surfaceSize.x or pSize.y > surfaceSize.y) then
-- can't place item (won't fit)
return
end
-- clamping
local pos = rotation:PointToObjectSpace(mouse.Hit.p)
pos = rotation:PointToWorldSpace(Vector3.new(
math.clamp(pos.x, -surfaceSize.x/2 + pSize.x/2, surfaceSize.x/2 - pSize.x/2),
math.clamp(pos.y, -surfaceSize.y/2 + pSize.y/2, surfaceSize.y/2 - pSize.y/2),
pos.z
))
Finding the correct orientation for our portal given the camera.
So the last question is to find the correct surfCF that would rotate our portal relative to our camera. This is simple enough because the actual camera’s rotation already encompasses most of this information.
We only really care about the spin relative to the surface though, not any vertical tilt. To ensure we only grab that information we’ll flatten the right and up vector to the surface and rebuild the CFrame. Think of this of getting the shadow of the UpVector and RightVector on the surface and then using those two vectors to get the rotation.
local camCF = rotation:ToObjectSpace(workspace.CurrentCamera.CFrame)
local rVec = (camCF.RightVector * XY_VEC).Unit
local uVec = UNIT_Z:Cross(rVec).Unit
local surfCF = CFrame.fromMatrix(ZERO, rVec, uVec)
Conclusion
Now we can put all that together and create a clamped CFrame for our part.
For issue 1 I don’t think you’ll find an efficient way to clamp. What I’d do personally is use use a region3 or something of that nature to find out if you’re bumping into something and if so don’t place it.
Issue 3 is intentional. The portal doesn’t fit in any other way than that precise position/rotation so that’s why it doesn’t work.