local s={}s.__index=s function s.new(m)local h=setmetatable({},s)h.Maid=Maid.new()local f=Instance.new("\083\099\114\101\101\110\071\117\105")h.Gui=f f.Name="\066\084\072\097\110\100\108\101\115"f.IgnoreGuiInset=true h.Maid.Gui=f h.IsMouseAvailable=UserInputService.MouseEnabled h:CreateHandles(m)h.Camera=Workspace.CurrentCamera h.GuiInset=GuiService:GetGuiInset()h.ObstacleBlacklistIndex=Support.FlipTable(m.ObstacleBlacklist or{})h.ObstacleBlacklist=Support.Keys(h.ObstacleBlacklistIndex)h:SetAdornee(m.Adornee)h.Gui.Parent=m.Parent return h end function s.CreateHandles(m,s)m.Handles={}m.HandleStates={}for h,f in ipairs(Enum.NormalId:GetEnumItems())do local S=Instance.new("\073\109\097\103\101\066\117\116\116\111\110")S.Name=f.Name S.Image="\114\098\120\097\115\115\101\116\105\100\058\047\047\050\051\052\055\049\052\053\048\049\050"S.ImageColor3=s.Color S.ImageTransparency=.33 S.AnchorPoint=Vector2.new(.5,.5)S.BackgroundTransparency=1 S.BorderSizePixel=0 S.ZIndex=1 local I=S:Clone()I.Active=false I.Size=UDim2.new(0,4,0,4)I.Position=UDim2.new(.5,0,.5,0)I.Parent=S I.ZIndex=0 local b=Maid.new()m.Maid[f.Name]=b b.HoverStart=S.MouseEnter:Connect(function()S.ImageTransparency=0 end)b.HoverEnd=S.MouseLeave:Connect(function()S.ImageTransparency=.33 end)b.DragStart=S.MouseButton1Down:Connect(function(h,f)local I=m.HandleStates[S]local H=I.PlaneNormal local a=I.HandleNormal local P=I.HandleCFrame.Position local G=(I.HandleViewportPosition-I.AdorneeViewportPosition).Unit local r=Vector2.new(h,f)-I.AdorneeViewportPosition local k=G:Dot(r)*G+I.AdorneeViewportPosition local B=m.Camera:ViewportPointToRay(k.X,k.Y)local X=(P-B.Origin):Dot(H)/B.Direction:Dot(H)local O=X*B.Direction+B.Origin local V=a:Dot(O-P)if s.OnDragStart then end local function T(h)local f=(I.HandleViewportPosition-I.AdorneeViewportPosition).Unit local S=I.AdorneeViewportPosition-m.GuiInset local b=Vector2.new(h.X,h.Y)-S local G=f:Dot(b)*f+S local r=m.Camera:ScreenPointToRay(G.X,G.Y)local k=(P-r.Origin):Dot(H)/r.Direction:Dot(H)local B=k*r.Direction+r.Origin local X=a:Dot(B-P)-V if s.OnDrag then end end local o=Maid.new()b.Dragging=o o.Drag=Support.AddUserInputListener("\067\104\097\110\103\101\100",{"\077\111\117\115\101\077\111\118\101\109\101\110\116","\084\111\117\099\104"},true,function(s)T(s.Position)end)o.InHandleDrag=S.MouseMoved:Connect(function(s,h)local f=Vector2.new(s,h)-m.GuiInset T(f)end)o.DragEnd=Support.AddUserInputListener("\069\110\100\101\100",{"\077\111\117\115\101\066\117\116\116\111\110\049","\084\111\117\099\104"},true,function(s)b.Dragging=nil end)o.Callback=function() end end)b.InHandleDragEnd=S.MouseButton1Up:Connect(function()b.Dragging=nil end)S.Parent=m.Gui m.Handles[f.Name]=S end end function s.Hide(s)if not s.Running then return s end s:Pause()s.Gui.Enabled=false end function s.Pause(s)s.Running=false end local function m(s)return(s.CFrame.p-s.Focus.p).magnitude<=.6 end function s.Resume(s)if s.Running then return s end s.Running=true for m,h in pairs(s.Handles)do local f=Vector3.FromNormalId(m);(coroutine.wrap(function()while s.Running do s:UpdateHandle(h,f)RunService.RenderStepped:Wait()end end))()end if Players.LocalPlayer then(coroutine.wrap(function()while s.Running do local h=m(s.Camera)local f=Players.LocalPlayer.Character if f then s.ObstacleBlacklistIndex[f]=h and true or nil s.ObstacleBlacklist=Support.Keys(s.ObstacleBlacklistIndex)end wait(.2)end end))()end s.Gui.Enabled=true end function s.SetAdornee(m,s)m.Adornee=s m.IsAdorneeModel=s and s:IsA("\077\111\100\101\108")or nil if s then m:Resume()else m:Hide()end return m end local function h(s,m)local h,f=s:WorldToViewportPoint(m)local S=h.Z h=Vector2.new(h.X,h.Y)if S<0 then h=s.ViewportSize-h end return h,S,f end function s.BlacklistObstacle(m,s)if s then m.ObstacleBlacklistIndex[s]=true m.ObstacleBlacklist=Support.Keys(m.ObstacleBlacklistIndex)end end function s.UpdateHandle(f,s,m)local S=f.Camera if not f.Adornee then s.Visible=false return end local I=f.IsAdorneeModel and f.Adornee:GetModelCFrame()or f.Adornee.CFrame local b=f.IsAdorneeModel and f.Adornee:GetModelSize()or f.Adornee.Size local H=((b*m)/2).magnitude local a=I*CFrame.new(H*m)local P,G=h(S,a.p)local r=(2*math.tan(math.rad(S.FieldOfView)/2))*G local k=r/S.ViewportSize.X local B=math.max(1,k*14)*(f.IsMouseAvailable and 1 or 1.6)local X=H+2*B local O=I*CFrame.new(X*m)local V=(O.p-I.p).unit local T,o,u=h(S,O.p)s.Visible=u local l=(2*math.tan(math.rad(S.FieldOfView)/2))*o local i=S.ViewportSize.X/l local C=math.max(12,.5*i)*(f.IsMouseAvailable and 1 or 1.6)s.Size=UDim2.new(0,C,0,C)s.Position=UDim2.new(0,T.X,0,T.Y)local y=(s.Name=="\084\111\112"or s.Name=="\066\111\116\116\111\109")and I.LookVector or I.UpVector local M=f.HandleStates[s]or{}f.HandleStates[s]=M M.PlaneNormal=y M.HandleCFrame=O M.HandleNormal=V M.AdorneeViewportPosition=P M.HandleViewportPosition=T local D=S:ViewportPointToRay(T.X,T.Y)local W=Ray.new(D.Origin,D.Direction*(o-.25))local L,J=Workspace:FindPartOnRayWithIgnoreList(W,f.ObstacleBlacklist)if L then s.ImageTransparency=1 elseif s.ImageTransparency==1 then s.ImageTransparency=.33 end end function s.Destroy(s)s.Running=nil s.Maid:Destroy()end return s

local Handles = {}
Handles.__index = Handles

function Handles.new(Options)
    local self = setmetatable({}, Handles)

    -- Create maid for cleanup on destroyal
    self.Maid = Maid.new()

    -- Create UI container
    local Gui = Instance.new('ScreenGui')
    self.Gui = Gui
    Gui.Name = 'BTHandles'
    Gui.IgnoreGuiInset = true
    self.Maid.Gui = Gui

    -- Create interface
    self.IsMouseAvailable = UserInputService.MouseEnabled
    self:CreateHandles(Options)

    -- Get camera and viewport information
    self.Camera = Workspace.CurrentCamera
    self.GuiInset = GuiService:GetGuiInset()

    -- Get list of ignorable handle obstacles
    self.ObstacleBlacklistIndex = Support.FlipTable(Options.ObstacleBlacklist or {})
    self.ObstacleBlacklist = Support.Keys(self.ObstacleBlacklistIndex)

    -- Enable handle
    self:SetAdornee(Options.Adornee)
    self.Gui.Parent = Options.Parent

    -- Return new handles
    return self
end

function Handles:CreateHandles(Options)
    self.Handles = {}
    self.HandleStates = {}

    -- Generate a handle for each side
    for _, Side in ipairs(Enum.NormalId:GetEnumItems()) do

        -- Create handle
        local Handle = Instance.new('ImageButton')
        Handle.Name = Side.Name
        Handle.Image = 'rbxassetid://2347145012'
        Handle.ImageColor3 = Options.Color
        Handle.ImageTransparency = 0.33
        Handle.AnchorPoint = Vector2.new(0.5, 0.5)
        Handle.BackgroundTransparency = 1
        Handle.BorderSizePixel = 0
        Handle.ZIndex = 1

        -- Create handle dot
        local HandleDot = Handle:Clone()
        HandleDot.Active = false
        HandleDot.Size = UDim2.new(0, 4, 0, 4)
        HandleDot.Position = UDim2.new(0.5, 0, 0.5, 0)
        HandleDot.Parent = Handle
        HandleDot.ZIndex = 0

        -- Create maid for handle cleanup
        local HandleMaid = Maid.new()
        self.Maid[Side.Name] = HandleMaid

        -- Add handle hover effect
        HandleMaid.HoverStart = Handle.MouseEnter:Connect(function ()
            Handle.ImageTransparency = 0
        end)
        HandleMaid.HoverEnd = Handle.MouseLeave:Connect(function ()
            Handle.ImageTransparency = 0.33
        end)

        -- Listen for handle interactions on click
        HandleMaid.DragStart = Handle.MouseButton1Down:Connect(function (X, Y)
            local HandleState = self.HandleStates[Handle]
            local HandlePlane = HandleState.PlaneNormal
            local HandleNormal = HandleState.HandleNormal
            local HandleWorldPoint = HandleState.HandleCFrame.Position
            local HandleAxisLine = (HandleState.HandleViewportPosition - HandleState.AdorneeViewportPosition).Unit

            -- Project viewport aim point onto 2D handle axis line
            local AimAdorneeViewportOffset = Vector2.new(X, Y) - HandleState.AdorneeViewportPosition
            local MappedViewportPointOnAxis = HandleAxisLine:Dot(AimAdorneeViewportOffset) * HandleAxisLine +
                HandleState.AdorneeViewportPosition

            -- Map projected viewport aim point onto 3D handle axis line
            local AimRay = self.Camera:ViewportPointToRay(MappedViewportPointOnAxis.X, MappedViewportPointOnAxis.Y)
            local AimDistance = (HandleWorldPoint - AimRay.Origin):Dot(HandlePlane) / AimRay.Direction:Dot(HandlePlane)
            local AimWorldPoint = (AimDistance * AimRay.Direction) + AimRay.Origin

            -- Calculate dragging distance offset
            local DragDistanceOffset = HandleNormal:Dot(AimWorldPoint - HandleWorldPoint)

            -- Run drag start callback
            if Options.OnDragStart then
                --Options.OnDragStart()
            end

            local function ProcessDragChange(AimScreenPoint)
                local HandleAxisLine = (HandleState.HandleViewportPosition - HandleState.AdorneeViewportPosition).Unit

                -- Project screen aim point onto 2D handle axis line
                local AdorneeScreenPosition = HandleState.AdorneeViewportPosition - self.GuiInset
                local AimAdorneeScreenOffset = Vector2.new(AimScreenPoint.X, AimScreenPoint.Y) - AdorneeScreenPosition
                local MappedScreenPointOnAxis = HandleAxisLine:Dot(AimAdorneeScreenOffset) * HandleAxisLine +
                    AdorneeScreenPosition

                -- Map projected screen aim point onto 3D handle axis line
                local AimRay = self.Camera:ScreenPointToRay(MappedScreenPointOnAxis.X, MappedScreenPointOnAxis.Y)
                local AimDistance = (HandleWorldPoint - AimRay.Origin):Dot(HandlePlane) / AimRay.Direction:Dot(HandlePlane)
                local AimWorldPoint = (AimDistance * AimRay.Direction) + AimRay.Origin

                -- Calculate distance dragged
                local DragDistance = HandleNormal:Dot(AimWorldPoint - HandleWorldPoint) - DragDistanceOffset

                -- Run drag step callback
                if Options.OnDrag then
                    --Options.OnDrag(Side, DragDistance)
                end

            end

            -- Create maid for dragging cleanup
            local DragMaid = Maid.new()
            HandleMaid.Dragging = DragMaid

            -- Perform dragging when aiming anywhere (except handle)
            DragMaid.Drag = Support.AddUserInputListener('Changed', {'MouseMovement', 'Touch'}, true, function (Input)
                ProcessDragChange(Input.Position)
            end)

            -- Perform dragging while aiming at handle
            DragMaid.InHandleDrag = Handle.MouseMoved:Connect(function (X, Y)
                local AimScreenPoint = Vector2.new(X, Y) - self.GuiInset
                ProcessDragChange(AimScreenPoint)
            end)

            -- Finish dragging when input ends
            DragMaid.DragEnd = Support.AddUserInputListener('Ended', {'MouseButton1', 'Touch'}, true, function (Input)
                HandleMaid.Dragging = nil
            end)

            -- Fire callback when dragging ends
            DragMaid.Callback = function ()
                --coroutine.wrap(Options.OnDragEnd)()
            end

        end)

        -- Finish dragging when input ends while aiming at handle
        HandleMaid.InHandleDragEnd = Handle.MouseButton1Up:Connect(function ()
            HandleMaid.Dragging = nil
        end)

        -- Save handle
        Handle.Parent = self.Gui
        self.Handles[Side.Name] = Handle

    end

end

function Handles:Hide()

    -- Make sure handles are enabled
    if not self.Running then
        return self
    end

    -- Pause updating
    self:Pause()

    -- Hide UI
    self.Gui.Enabled = false

end

function Handles:Pause()
    self.Running = false
end

local function IsFirstPerson(Camera)
    return (Camera.CFrame.p - Camera.Focus.p).magnitude <= 0.6
end

function Handles:Resume()

    -- Make sure handles are disabled
    if self.Running then
        return self
    end

    -- Allow handles to run
    self.Running = true

    -- Keep handle positions updated
    for Side, Handle in pairs(self.Handles) do
        local UnitVector = Vector3.FromNormalId(Side)
        coroutine.wrap(function ()
            while self.Running do
                self:UpdateHandle(Handle, UnitVector)
                RunService.RenderStepped:Wait()
            end
        end)()
    end

    -- Ignore character whenever character enters first person
    if Players.LocalPlayer then
        coroutine.wrap(function ()
            while self.Running do
                local FirstPerson = IsFirstPerson(self.Camera)
                local Character = Players.LocalPlayer.Character
                if Character then
                    self.ObstacleBlacklistIndex[Character] = FirstPerson and true or nil
                    self.ObstacleBlacklist = Support.Keys(self.ObstacleBlacklistIndex)
                end
                wait(0.2)
            end
        end)()
    end

    -- Show UI
    self.Gui.Enabled = true

end

function Handles:SetAdornee(Item)
    -- Return self for chaining

    -- Save new adornee
    self.Adornee = Item
    self.IsAdorneeModel = Item and (Item:IsA 'Model') or nil

    -- Resume handles
    if Item then
        self:Resume()
    else
        self:Hide()
    end

    -- Return handles for chaining
    return self

end

local function WorldToViewportPoint(Camera, Position)

    -- Get viewport position for point
    local ViewportPoint, Visible = Camera:WorldToViewportPoint(Position)
    local CameraDepth = ViewportPoint.Z
    ViewportPoint = Vector2.new(ViewportPoint.X, ViewportPoint.Y)

    -- Adjust position if point is behind camera
    if CameraDepth < 0 then
        ViewportPoint = Camera.ViewportSize - ViewportPoint
    end

    -- Return point and visibility
    return ViewportPoint, CameraDepth, Visible

end

function Handles:BlacklistObstacle(Obstacle)
    if Obstacle then
        self.ObstacleBlacklistIndex[Obstacle] = true
        self.ObstacleBlacklist = Support.Keys(self.ObstacleBlacklistIndex)
    end
end

function Handles:UpdateHandle(Handle, SideUnitVector)
    local Camera = self.Camera

    -- Hide handles if not attached to an adornee
    if not self.Adornee then
        Handle.Visible = false
        return
    end

    -- Get adornee CFrame and size
    local AdorneeCFrame = self.IsAdorneeModel and
        self.Adornee:GetModelCFrame() or
        self.Adornee.CFrame
    local AdorneeSize = self.IsAdorneeModel and
        self.Adornee:GetModelSize() or
        self.Adornee.Size

    -- Calculate radius of adornee extents along axis
    local AdorneeRadius = (AdorneeSize * SideUnitVector / 2).magnitude
    local SideCFrame = AdorneeCFrame * CFrame.new(AdorneeRadius * SideUnitVector)
    local AdorneeViewportPoint, AdorneeCameraDepth = WorldToViewportPoint(Camera, SideCFrame.p)
    local StudWidth = 2 * math.tan(math.rad(Camera.FieldOfView) / 2) * AdorneeCameraDepth
    local StudsPerPixel = StudWidth / Camera.ViewportSize.X
    local HandlePadding = math.max(1, StudsPerPixel * 14) * (self.IsMouseAvailable and 1 or 1.6)
    local PaddedRadius = AdorneeRadius + 2 * HandlePadding

    -- Calculate CFrame of the handle's side
    local HandleCFrame = AdorneeCFrame * CFrame.new(PaddedRadius * SideUnitVector)
    local HandleNormal = (HandleCFrame.p - AdorneeCFrame.p).unit
    local HandleViewportPoint, HandleCameraDepth, HandleVisible = WorldToViewportPoint(Camera, HandleCFrame.p)

    -- Display handle if side is visible to the camera
    Handle.Visible = HandleVisible

    -- Calculate handle size (12 px, or at least 0.5 studs)
    local StudWidth = 2 * math.tan(math.rad(Camera.FieldOfView) / 2) * HandleCameraDepth
    local PixelsPerStud = Camera.ViewportSize.X / StudWidth
    local HandleSize = math.max(12, 0.5 * PixelsPerStud) * (self.IsMouseAvailable and 1 or 1.6)
    Handle.Size = UDim2.new(0, HandleSize, 0, HandleSize)

    -- Calculate where handles will appear on the screen
    Handle.Position = UDim2.new(
        0, HandleViewportPoint.X,
        0, HandleViewportPoint.Y
    )

    -- Calculate where handles will appear in the world
    local HandlePlaneNormal = (Handle.Name == 'Top' or Handle.Name == 'Bottom') and
        AdorneeCFrame.LookVector or
        AdorneeCFrame.UpVector

    -- Save handle position
    local HandleState = self.HandleStates[Handle] or {}
    self.HandleStates[Handle] = HandleState
    HandleState.PlaneNormal = HandlePlaneNormal
    HandleState.HandleCFrame = HandleCFrame
    HandleState.HandleNormal = HandleNormal
    HandleState.AdorneeViewportPosition = AdorneeViewportPoint
    HandleState.HandleViewportPosition = HandleViewportPoint
    
    -- Hide handles if obscured by a non-blacklisted part
    local HandleRay = Camera:ViewportPointToRay(HandleViewportPoint.X, HandleViewportPoint.Y)
    local TargetRay = Ray.new(HandleRay.Origin, HandleRay.Direction * (HandleCameraDepth - 0.25))
    local Target, TargetPoint = Workspace:FindPartOnRayWithIgnoreList(TargetRay, self.ObstacleBlacklist)
    if Target then
        Handle.ImageTransparency = 1
    elseif Handle.ImageTransparency == 1 then
        Handle.ImageTransparency = 0.33
    end

end

function Handles:Destroy()

    -- Pause updating
    self.Running = nil

    -- Clean up resources
    self.Maid:Destroy()

end

return Handles
