When I set the Orientation of a BasePart to a vector, all of whose elements are divisible by 90 but not 180, the resulting rotation of the BasePart.CFrame is ever so slightly off:
local Part = Instance.new("Part")
Part.Orientation = Vector3.new(90)
print(Part.CFrame)
I suspect this stems from some rounding error in the intermediate calculation even though the starting orientation in degrees and the expected resultant CFrame can both be represented exactly in floating point.
Possible fix: Using sinpi(x) instead of sin(M_PI*x) in the calculation, or similar.
This is the code the Roblox Studio draggers use to fixup very small floating point error into precise orthogonal rotations if you want a workaround:
local function fixSlightlyOffgridOrientation(cf: CFrame): CFrame
-- Get the components
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cf:GetComponents()
-- Restore any values which are very close to zero to zero.
-- This code is fairly ugly but this is used on a performance critical
-- codepath so it must be maximally unrolled rather than being restructured
-- into some form of loop over a table.
-- Typically the values to be corrected will be ~3e-8 so we only need a
-- very small threshold and keeping the threshold especially small avoids
-- impacting any actual use cases.
local THRESHOLD = 1e-5
local clamped = false
if math.abs(r00) < THRESHOLD and r00 ~= 0 then
r00 = 0
clamped = true
end
if math.abs(r01) < THRESHOLD and r01 ~= 0 then
r01 = 0
clamped = true
end
if math.abs(r02) < THRESHOLD and r02 ~= 0 then
r02 = 0
clamped = true
end
if math.abs(r10) < THRESHOLD and r10 ~= 0 then
r10 = 0
clamped = true
end
if math.abs(r11) < THRESHOLD and r11 ~= 0 then
r11 = 0
clamped = true
end
if math.abs(r12) < THRESHOLD and r12 ~= 0 then
r12 = 0
clamped = true
end
if math.abs(r20) < THRESHOLD and r20 ~= 0 then
r20 = 0
clamped = true
end
if math.abs(r21) < THRESHOLD and r21 ~= 0 then
r21 = 0
clamped = true
end
if math.abs(r22) < THRESHOLD and r22 ~= 0 then
r22 = 0
clamped = true
end
if clamped then
-- If we restored anything to zero, also restore things close to 1.
-- We should only do this if something was clamped to zero because
-- values close to 1 are much more sensitive. With a small angle theta
-- the values above will change by ~theta but the values below will
-- only change by 1-cos(theta) which asymtotically approaches zero
-- for the small theta.
if math.abs(r00 - 1) < THRESHOLD then
r00 = 1
elseif math.abs(r00 + 1) < THRESHOLD then
r00 = -1
end
if math.abs(r01 - 1) < THRESHOLD then
r01 = 1
elseif math.abs(r01 + 1) < THRESHOLD then
r01 = -1
end
if math.abs(r02 - 1) < THRESHOLD then
r02 = 1
elseif math.abs(r02 + 1) < THRESHOLD then
r02 = -1
end
if math.abs(r10 - 1) < THRESHOLD then
r10 = 1
elseif math.abs(r10 + 1) < THRESHOLD then
r10 = -1
end
if math.abs(r11 - 1) < THRESHOLD then
r11 = 1
elseif math.abs(r11 + 1) < THRESHOLD then
r11 = -1
end
if math.abs(r12 - 1) < THRESHOLD then
r12 = 1
elseif math.abs(r12 + 1) < THRESHOLD then
r12 = -1
end
if math.abs(r20 - 1) < THRESHOLD then
r20 = 1
elseif math.abs(r20 + 1) < THRESHOLD then
r20 = -1
end
if math.abs(r21 - 1) < THRESHOLD then
r21 = 1
elseif math.abs(r21 + 1) < THRESHOLD then
r21 = -1
end
if math.abs(r22 - 1) < THRESHOLD then
r22 = 1
elseif math.abs(r22 + 1) < THRESHOLD then
r22 = -1
end
return CFrame.new(x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)
else
return cf
end
end