diff --git a/lua/vgui/acf_panel.lua b/lua/vgui/acf_panel.lua index 32823dd7b..dd8a6511e 100644 --- a/lua/vgui/acf_panel.lua +++ b/lua/vgui/acf_panel.lua @@ -229,17 +229,83 @@ function PANEL:AddCollapsible(Text, State) return Base, Category end +-- Lerps linearly between two matrices +-- This is in no way correct, but works fine for this purpose +-- Matrices need to be affine, shear is not preserved +local function LerpMatrix(A, B, t) + if not A:IsRotationMatrix() or not B:IsRotationMatrix() then + return B + end + + t = math.Clamp(t, 0, 1) + + local Pos = A:GetTranslation() * (1 - t) + B:GetTranslation() * t + + -- rotate A angle towards B angle + local Ang = A:GetAngles() + local A_Dir = A:GetForward() + local B_Dir = B:GetForward() + + local Dot = A_Dir:Dot(B_Dir) + if Dot < 0.999999 then + RotAxis = A_Dir:Cross(B_Dir) + RotAxis:Normalize() + + RotAngle = 180 / math.pi * math.acos(Dot) + Ang:RotateAroundAxis(RotAxis, RotAngle * t) + end + + local Scale = A:GetScale() * (1 - t) + B:GetScale() * t + + local C = Matrix() + C:SetTranslation(Pos) + C:SetAngles(Ang) + C:SetScale(Scale) + + return C +end + +-- Rotates the matrix roll towards zero +local function RotateMatrixRollToZero(A, t) + local Angles = A:GetAngles() + Angles:RotateAroundAxis(A:GetForward(), -t * Angles.r) + + local B = Matrix(A) + B:SetAngles(Angles) + + return B +end + +-- Returns the distance from P to the closest point on a box +-- From https://iquilezles.org/articles/distfunctions/ +local function BoxSDF(P, Box) + local Q = Vector(math.abs(P.x), math.abs(P.y), math.abs(P.z)) - Box + + local D1 = Vector(math.max(0, Q.x), math.max(0, Q.y), math.max(0, Q.z)):Length() + local D2 = math.min(math.max(Q.x, math.max(Q.y, Q.z)), 0) + + return D1 + D2 +end + function PANEL:AddModelPreview(Model, Rotate) local Settings = { - Height = 120, - FOV = 90, + Height = 120, + FOV = 60, + + Pitch = 15, -- Default pitch angle, camera will kinda bob up and down with nonzero setting + Rotation = Angle(0, -35, 0) -- Default rotation rate } local Panel = self:AddPanel("DModelPanel") Panel.Rotate = tobool(Rotate) - Panel.Rotation = Angle(0, -75) Panel.Settings = Settings -- Storing the default settings + Panel.IsMouseDown = false + Panel.InitialMouseOffset = Vector(0, 0) + Panel.LastMouseOffset = Vector(0, 0) + + Panel.RotationDirection = 1 + function Panel:SetRotateModel(Bool) self.Rotate = tobool(Bool) end @@ -256,7 +322,7 @@ function PANEL:AddModelPreview(Model, Rotate) function Panel:UpdateModel(Path) if not isstring(Path) then - Path = "models/props_junk/PopCan01a.mdl" + return self:DrawEntity(false) end local Center = ModelData.GetModelCenter(Path) @@ -273,14 +339,31 @@ function PANEL:AddModelPreview(Model, Rotate) local Size = ModelData.GetModelSize(Path) + local StartMatrix = Matrix() + + -- looks a bit nicer with this + if string.find(Path, "engines") ~= nil then + StartMatrix:Rotate(Angle(self.Settings.Pitch, 0, 0)) + elseif string.find(Path, "reciever") ~= nil then + StartMatrix:Rotate(Angle(self.Settings.Pitch, 180, 0)) + else + StartMatrix:Rotate(Angle(self.Settings.Pitch, -90, 0)) + end + + self.RotMatrix = Matrix(StartMatrix) + self.TargetRotMatrix = Matrix(StartMatrix) + self.CamCenter = Center - self.CamOffset = Vector(0, Size:Length()) - self.LastTime = RealTime() + self.CamDistance = 1.2 * math.max(Size.x, math.max(Size.y, Size.z)) + + self.BoxSize = Vector(Size) -- Used for zooming + + self.CamDistanceMul = 1 + self.CamDistanceMulTarget = 1 self:DrawEntity(true) self:SetModel(Path) - self:SetLookAt(Center) - self:SetCamPos(Center + self.CamOffset) + self:SetCamPos(Center + Vector(-self.CamDistance, 0, 0)) end function Panel:UpdateSettings(Data) @@ -290,6 +373,35 @@ function PANEL:AddModelPreview(Model, Rotate) self:SetFOV(Data and Data.FOV or Settings.FOV) end + function Panel:OnMousePressed(Button) + if Button ~= MOUSE_LEFT then return end + + local MouseOffset = Vector(self:ScreenToLocal(input.GetCursorPos())) + + if MouseOffset:WithinAABox(Vector(0, 0, -1), Vector(self:GetWide(), self:GetTall(), 1)) then + self.IsMouseDown = true + self.InitialMouseOffset = MouseOffset + self.LastMouseOffset = MouseOffset + end + end + + function Panel:OnMouseReleased_impl(Button) + if Button ~= MOUSE_LEFT then return end + + self.IsMouseDown = false + + -- Reset target angles + self.TargetRotMatrix:SetAngles(Angle(self.Settings.Pitch, self.TargetRotMatrix:GetAngles().y, 0)) + + -- Find what direction user was rotating, and keep rotating in the same direction + local YawDiff = self.TargetRotMatrix:GetAngles().y - self.RotMatrix:GetAngles().y + self.RotationDirection = (YawDiff <= 0) and 1 or -1 + end + + function Panel:OnMouseReleased(Button) + self:OnMouseReleased_impl(Button) + end + function Panel:LayoutEntity() if self.NotDrawn then return end @@ -298,18 +410,61 @@ function PANEL:AddModelPreview(Model, Rotate) end if not self.Rotate then return end - if not self.CamOffset then return end + if not self.RotMatrix then return end - local Time = RealTime() - local Delta = Time - self.LastTime - local Rotation = self.Rotation * Delta - local Offset = self.CamOffset + -- Handle mouse movement + if self.IsMouseDown then + local MouseOffset = Vector(self:ScreenToLocal(input.GetCursorPos())) + local Delta = MouseOffset - self.LastMouseOffset - Offset:Rotate(Rotation) + -- Rotate towards mouse movement + local Rotation = Angle(Delta.y * 0.7, -Delta.x, 0) * FrameTime() * 48 - self:SetCamPos(self.CamCenter + Offset) + Rotation.p = math.Clamp(Rotation.p, -5, 5) + Rotation.y = math.Clamp(Rotation.y, -15, 15) - self.LastTime = Time + self.TargetRotMatrix:Rotate(Rotation) + + self.LastMouseOffset = MouseOffset + else + -- Spin around like normal when not panning + self.TargetRotMatrix:Rotate(self.Settings.Rotation * FrameTime() * self.RotationDirection) + end + + -- Lerp rotation towards target + local LerpT = math.Clamp(FrameTime() * 4, 0.05, 0.3) + self.RotMatrix = LerpMatrix(self.RotMatrix, self.TargetRotMatrix, LerpT) + + -- Rotate roll towards zero when not panning + if not self.IsMouseDown then + self.RotMatrix = RotateMatrixRollToZero(self.RotMatrix, LerpT * 0.25) + end + + -- Compute zoom distance + if self.IsMouseDown and input.IsMouseDown(MOUSE_RIGHT) then + local DistToBox = BoxSDF(self.RotMatrix * Vector(-self.CamDistance, 0, 0), self.BoxSize) + local Fraction = math.pow(1 - DistToBox / self.CamDistance, 0.5) + + self.CamDistanceMulTarget = Fraction + else + self.CamDistanceMulTarget = 1 + end + + -- Lerp distance towards target + self.CamDistanceMul = self.CamDistanceMul + (self.CamDistanceMulTarget - self.CamDistanceMul) * LerpT * 0.5 + + local CamTransform = Matrix() + CamTransform:Translate(self.CamCenter) + CamTransform:Rotate(self.RotMatrix:GetAngles()) + CamTransform:Translate(Vector(-self.CamDistance * self.CamDistanceMul, 0, 0)) + + self:SetLookAng(CamTransform:GetAngles()) + self:SetCamPos(CamTransform:GetTranslation()) + + -- Mousedown state gets "stuck" if cursor is outside the panel when releasing, so, adding this + if self.IsMouseDown and not input.IsMouseDown(MOUSE_LEFT) then + self:OnMouseReleased_impl(MOUSE_LEFT) + end end Panel:UpdateModel(Model)