My bad, I’ve just realised why. I had mispelled math.sign
and wrote it as math.sin
- will edit the original to fix it. Your handles are looking pretty good though!
As an apology for messing up the spelling earlier, this might be of use to you:
Example transform gizmo
-- demo parts to visualise
local partA = Instance.new('Part')
partA.Name = 'Start_PartA'
partA.Size = Vector3.one*4
partA.CFrame = CFrame.new(0, 2, 0)
partA.Anchored = true
partA.BrickColor = BrickColor.Green()
partA.Parent = workspace
local gizmo = Instance.new('Handles')
gizmo.Faces = Faces.new(Enum.NormalId.Front)
gizmo.Style = Enum.HandlesStyle.Movement
gizmo.Adornee = partA
gizmo.Parent = partA
-- function to compute the angle between
-- two direction vectors on the specified axis
--
-- where 'a' & 'b' are direction vectors
-- and 'axis' is the axis of rotation
--
local function computeSignedAngle(a, b, axis)
axis = axis or Vector3.yAxis
local sqrA = a.X*a.X + a.Y*a.Y + a.Z*a.Z
local sqrB = b.X*b.X + b.Y*b.Y + b.Z*b.Z
local d = math.sqrt(sqrA * sqrB)
if d < 1e-6 then
return 0
end
d = math.clamp(a:Dot(b) / d, -1, 1)
d = math.acos(d)
local c = a:Cross(b)
local s = math.sign(axis.X*c.X + axis.Y*c.Y + axis.Z*c.Z)
d *= s
return d
end
-- compute the ray-plane intersection point
local function rayPlaneIntersect(origin, direction, planePoint, planeNormal)
local d = direction:Dot(planeNormal)
d = (planePoint - origin):Dot(planeNormal) / d
return origin + direction*d
end
-- screen-world ray-plane intersect
local function computeIntersect(camera, mousePos, translation, planeNormal)
local ray = camera:ViewportPointToRay(mousePos.X, mousePos.Y)
local intersect = rayPlaneIntersect(ray.Origin, ray.Direction, translation, planeNormal)
return intersect
end
-- demo main
local UserInputService = game:GetService('UserInputService')
local MULTIPLIER = 0.1
local AXES = { Enum.Axis.X, Enum.Axis.Y, Enum.Axis.Z }
local PIVOT = {
[Enum.Axis.X] = Enum.Axis.Y,
[Enum.Axis.Y] = Enum.Axis.Z,
[Enum.Axis.Z] = Enum.Axis.X,
}
local camera = workspace.CurrentCamera
local isRotating = false
local rotationAxis = 1
local prevTransform = nil
local function updateAdornee()
-- just used to update the demo so we can visualise the rotation
local axis = AXES[rotationAxis]
print('Selected:', axis.Name)
local normalId
if axis == Enum.Axis.X then
normalId = Enum.NormalId.Front
elseif axis == Enum.Axis.Y then
normalId = Enum.NormalId.Top
elseif axis == Enum.Axis.Z then
normalId = Enum.NormalId.Right
end
gizmo.Faces = Faces.new(normalId)
gizmo.Color3 = Color3.fromHSV(rotationAxis / 3, 0.8, 0.75)
end
updateAdornee()
UserInputService.InputBegan:Connect(function (input, gameProcessed)
if gameProcessed then
return
end
local inputType = input.UserInputType
if inputType == Enum.UserInputType.MouseButton1 then
prevTransform = partA.CFrame
isRotating = true
elseif inputType == Enum.UserInputType.Keyboard then
-- Q / E to toggle the axis we want to rotate for demo purposes
local keyCode = input.KeyCode
local direction
if keyCode == Enum.KeyCode.E then
direction = 1
elseif keyCode == Enum.KeyCode.Q then
direction = -1
end
if direction then
rotationAxis += direction
rotationAxis = rotationAxis > 3 and 1 or (rotationAxis < 1 and 3 or rotationAxis)
updateAdornee()
end
end
end)
UserInputService.InputChanged:Connect(function (input, gameProcessed)
if gameProcessed or not isRotating then
return
end
local inputType = input.UserInputType
if inputType ~= Enum.UserInputType.MouseMovement then
return
end
local axis = AXES[rotationAxis]
local pivotAxis = PIVOT[axis]
axis = Vector3.FromAxis(axis)
pivotAxis = Vector3.FromAxis(pivotAxis)
local transform = partA.CFrame
local translation = transform.Position
-- i.e. compute the intersection point on the plane
-- and then measure the signed angle between our `axis x pivotAxis`
-- and the world rotation axis
local worldAxis = prevTransform:VectorToWorldSpace(pivotAxis)
local position = computeIntersect(camera, input.Position, translation, worldAxis)
local angle = computeSignedAngle(prevTransform:VectorToWorldSpace(axis:Cross(pivotAxis)), (position - translation).Unit, worldAxis)
-- update transform
partA.CFrame = prevTransform * CFrame.fromAxisAngle(pivotAxis, angle)
end)
UserInputService.InputEnded:Connect(function (input)
local inputType = input.UserInputType
if inputType ~= Enum.UserInputType.MouseButton1 then
return
end
isRotating = false
prevTransform = nil
end)
Or if you want to change the angle relative to the starting point:
Gizmo Example 2
-- demo parts to visualise
local partA = Instance.new('Part')
partA.Name = 'Start_PartA'
partA.Size = Vector3.one*4
partA.CFrame = CFrame.new(0, 2, 0)
partA.Anchored = true
partA.BrickColor = BrickColor.Green()
partA.Parent = workspace
local gizmo = Instance.new('Handles')
gizmo.Faces = Faces.new(Enum.NormalId.Front)
gizmo.Style = Enum.HandlesStyle.Movement
gizmo.Adornee = partA
gizmo.Parent = partA
-- function to compute the angle between
-- two direction vectors on the specified axis
--
-- where 'a' & 'b' are direction vectors
-- and 'axis' is the axis of rotation
--
local function computeSignedAngle(a, b, axis)
axis = axis or Vector3.yAxis
local sqrA = a.X*a.X + a.Y*a.Y + a.Z*a.Z
local sqrB = b.X*b.X + b.Y*b.Y + b.Z*b.Z
local d = math.sqrt(sqrA * sqrB)
if d < 1e-6 then
return 0
end
d = math.clamp(a:Dot(b) / d, -1, 1)
d = math.acos(d)
local c = a:Cross(b)
local s = math.sign(axis.X*c.X + axis.Y*c.Y + axis.Z*c.Z)
d *= s
return d
end
-- compute the ray-plane intersection point
local function rayPlaneIntersect(origin, direction, planePoint, planeNormal)
local d = direction:Dot(planeNormal)
d = (planePoint - origin):Dot(planeNormal) / d
return origin + direction*d
end
-- screen-world ray-plane intersect
local function computeIntersect(camera, mousePos, translation, planeNormal)
local ray = camera:ViewportPointToRay(mousePos.X, mousePos.Y)
local intersect = rayPlaneIntersect(ray.Origin, ray.Direction, translation, planeNormal)
return intersect
end
-- demo main
local UserInputService = game:GetService('UserInputService')
local MULTIPLIER = 0.1
local AXES = { Enum.Axis.X, Enum.Axis.Y, Enum.Axis.Z }
local PIVOT = {
[Enum.Axis.X] = Enum.Axis.Y,
[Enum.Axis.Y] = Enum.Axis.Z,
[Enum.Axis.Z] = Enum.Axis.X,
}
local camera = workspace.CurrentCamera
local isRotating = false
local rotationAxis = 1
local prevTransform = nil
local prevPosition = nil
local function updateAdornee()
-- just used to update the demo so we can visualise the rotation
local axis = AXES[rotationAxis]
print('Selected:', axis.Name)
local normalId
if axis == Enum.Axis.X then
normalId = Enum.NormalId.Front
elseif axis == Enum.Axis.Y then
normalId = Enum.NormalId.Top
elseif axis == Enum.Axis.Z then
normalId = Enum.NormalId.Right
end
gizmo.Faces = Faces.new(normalId)
gizmo.Color3 = Color3.fromHSV(rotationAxis / 3, 0.8, 0.75)
end
updateAdornee()
UserInputService.InputBegan:Connect(function (input, gameProcessed)
if gameProcessed then
return
end
local inputType = input.UserInputType
if inputType == Enum.UserInputType.MouseButton1 then
local axis = AXES[rotationAxis]
local pivotAxis = PIVOT[axis]
axis = Vector3.FromAxis(axis)
pivotAxis = Vector3.FromAxis(pivotAxis)
local transform = partA.CFrame
local translation = transform.Position
prevTransform = transform
prevPosition = computeIntersect(camera, input.Position, translation, prevTransform:VectorToWorldSpace(pivotAxis))
isRotating = true
elseif inputType == Enum.UserInputType.Keyboard then
-- Q / E to toggle the axis we want to rotate for demo purposes
local keyCode = input.KeyCode
local direction
if keyCode == Enum.KeyCode.E then
direction = 1
elseif keyCode == Enum.KeyCode.Q then
direction = -1
end
if direction then
rotationAxis += direction
rotationAxis = rotationAxis > 3 and 1 or (rotationAxis < 1 and 3 or rotationAxis)
updateAdornee()
end
end
end)
UserInputService.InputChanged:Connect(function (input, gameProcessed)
if gameProcessed or not isRotating then
return
end
local inputType = input.UserInputType
if inputType ~= Enum.UserInputType.MouseMovement then
return
end
local axis = AXES[rotationAxis]
local pivotAxis = PIVOT[axis]
axis = Vector3.FromAxis(axis)
pivotAxis = Vector3.FromAxis(pivotAxis)
local transform = partA.CFrame
local translation = transform.Position
-- i.e. compute the intersection point on the plane
-- and then measure the signed angle between our `axis x pivotAxis`
-- and the world rotation axis
local worldAxis = prevTransform:VectorToWorldSpace(pivotAxis)
local position = computeIntersect(camera, input.Position, translation, worldAxis)
local angle = computeSignedAngle((prevPosition - translation).Unit, (position - translation).Unit, worldAxis)
-- update transform
partA.CFrame = prevTransform * CFrame.fromAxisAngle(pivotAxis, angle)
end)
UserInputService.InputEnded:Connect(function (input)
local inputType = input.UserInputType
if inputType ~= Enum.UserInputType.MouseButton1 then
return
end
isRotating = false
prevPosition = nil
prevTransform = nil
end)