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

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
