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:
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:
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)