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

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
        Handle.Visible = false

        -- 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.Visible = true
        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
