Files
wnsrc/lua/pac3/editor/client/mctrl.lua
lifestorm 73479cff9e Upload
2024-08-04 22:55:00 +03:00

568 lines
19 KiB
Lua

--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
pace.mctrl = {}
local mctrl = pace.mctrl
mctrl.grab_dist = 15
mctrl.angle_pos = 0.5
local cvar_pos_grid = CreateClientConVar("pac_grid_pos_size", "4")
local cvar_ang_grid = CreateClientConVar("pac_grid_ang_size", "45")
--[[
Give this function the coordinates of a pixel on your screen, and it will return a unit vector pointing
in the direction that the camera would project that pixel in.
Useful for converting mouse positions to aim vectors for traces.
iScreenX is the x position of your cursor on the screen, in pixels.
iScreenY is the y position of your cursor on the screen, in pixels.
iScreenW is the width of the screen, in pixels.
iScreenH is the height of the screen, in pixels.
angCamRot is the angle your camera is at
fFoV is the Field of View (FOV) of your camera in ___radians___
Note: This must be nonzero or you will get a divide by zero error.
]]
local function LPCameraScreenToVector(iScreenX, iScreenY, iScreenW, iScreenH, angCamRot, fFoV)
--This code works by basically treating the camera like a frustrum of a pyramid.
--We slice this frustrum at a distance "d" from the camera, where the slice will be a rectangle whose width equals the "4:3" width corresponding to the given screen height.
local d = 4 * iScreenH / (6 * math.tan(0.5 * fFoV))
--Forward, right, and up vectors (need these to convert from local to world coordinates
local vForward = angCamRot:Forward()
local vRight = angCamRot:Right()
local vUp = angCamRot:Up()
--Then convert vec to proper world coordinates and return it
return (d * vForward + (iScreenX - 0.5 * iScreenW) * vRight + (0.5 * iScreenH - iScreenY) * vUp):GetNormalized()
end
--[[
Give this function a vector, pointing from the camera to a position in the world,
and it will return the coordinates of a pixel on your screen - this is where the world position would be projected onto your screen.
Useful for finding where things in the world are on your screen (if they are at all).
vDir is a direction vector pointing from the camera to a position in the world
iScreenW is the width of the screen, in pixels.
iScreenH is the height of the screen, in pixels.
angCamRot is the angle your camera is at
fFoV is the Field of View (FOV) of your camera in ___radians___
Note: This must be nonzero or you will get a divide by zero error.
Returns x, y, iVisibility.
x and y are screen coordinates.
iVisibility will be:
1 if the point is visible
0 if the point is in front of the camera, but is not visible
-1 if the point is behind the camera
]]
local function VectorToLPCameraScreen(vDir, iScreenW, iScreenH, angCamRot, fFoV)
--Same as we did above, we found distance the camera to a rectangular slice of the camera's frustrum, whose width equals the "4:3" width corresponding to the given screen height.
local d = 4 * iScreenH / (6 * math.tan(0.5 * fFoV))
local fdp = angCamRot:Forward():Dot(vDir)
--fdp must be nonzero ( in other words, vDir must not be perpendicular to angCamRot:Forward() )
--or we will get a divide by zero error when calculating vProj below.
if fdp == 0 then return 0, 0, -1 end
--Using linear projection, project this vector onto the plane of the slice
local vProj = (d / fdp) * vDir
--Dotting the projected vector onto the right and up vectors gives us screen positions relative to the center of the screen.
--We add half-widths / half-heights to these coordinates to give us screen positions relative to the upper-left corner of the screen.
--We have to subtract from the "up" instead of adding, since screen coordinates decrease as they go upwards.
local x = 0.5 * iScreenW + angCamRot:Right():Dot(vProj)
local y = 0.5 * iScreenH - angCamRot:Up():Dot(vProj)
--Lastly we have to ensure these screen positions are actually on the screen.
local iVisibility
--Simple check to see if the object is in front of the camera
if fdp < 0 then
iVisibility = -1
elseif x < 0 or x > iScreenW or y < 0 or y > iScreenH then
--We've already determined the object is in front of us, but it may be lurking just outside our field of vision.
iVisibility = 0
else
iVisibility = 1
end
return x, y, iVisibility
end
local function LocalToWorldAngle(lang, wang)
local lm = Matrix()
lm:SetAngles(lang)
local wm = Matrix()
wm:SetAngles(wang)
return (wm * lm):GetAngles()
end
local function WorldToLocalAngle(lang, wang)
local lm = Matrix()
lm:SetAngles(lang)
local wm = Matrix()
wm:SetAngles(wang)
return (wm:GetInverse() * lm):GetAngles()
end
local function cursor_pos()
local x, y = input.GetCursorPos()
if mctrl.grab and mctrl.grab.mouse_offset then
x = x + mctrl.grab.mouse_offset.x
y = y + mctrl.grab.mouse_offset.y
end
return x, y
end
-- pace
do
mctrl.target = NULL
function mctrl.SetTarget(part)
part = part or NULL
if not part:IsValid() then
mctrl.target = NULL
return
end
if not part.GetDrawPosition then
mctrl.target = NULL
else
mctrl.target = part
end
end
function mctrl.GetTarget()
return mctrl.target:IsValid() and not mctrl.target:IsHidden() and mctrl.target or NULL
end
function mctrl.GetAxes(ang)
return ang:Forward(), ang:Right() * -1, ang:Up()
end
function mctrl.GetWorldPosition()
local part = mctrl.GetTarget()
if not part:IsValid() then return end
local m = part:GetWorldMatrixWithoutOffsets()
return m:GetTranslation(), m:GetAngles()
end
function mctrl.GetWorldMatrix()
local part = mctrl.GetTarget()
if not part:IsValid() then return end
return part:GetWorldMatrixWithoutOffsets()
end
function mctrl.WorldToLocalPosition(pos, ang)
local part = mctrl.GetTarget()
if not part:IsValid() then return end
local wpos, wang = part:GetBonePosition()
if wpos and wang then return WorldToLocal(pos, ang, wpos, wang) end
end
function mctrl.GetCameraFOV()
if pace.editing_viewmodel or pace.editing_hands then return pac.LocalPlayer:GetActiveWeapon().ViewModelFOV or 55 end
return pace.GetViewFOV()
end
function mctrl.VecToScreen(vec)
local x, y, vis = VectorToLPCameraScreen((vec - EyePos()):GetNormalized(), ScrW(), ScrH(), EyeAngles(), math.rad(mctrl.GetCameraFOV()))
return {
x = x - 1,
y = y - 1,
visible = vis == 1
}
end
function mctrl.ScreenToVec(x, y)
local vec = LPCameraScreenToVector(x, y, ScrW(), ScrH(), EyeAngles(), math.rad(mctrl.GetCameraFOV()))
return vec
end
function mctrl.GetGizmoSize()
local part = pace.current_part
if pace.editing_viewmodel or pace.editing_hands then return 5 end
if part.ClassName == "clip" or part.ClassName == "clip2" then
part = part.Parent
end
if part.ClassName == "camera" then return 30 end
if part.ClassName == "group" then return 45 end
if not part:IsValid() or not part.GetWorldPosition then return 3 end
local dist = (part:GetWorldMatrixWithoutOffsets():GetTranslation():Distance(pace.GetViewPos()) / 50)
if dist > 1 then
dist = 1 / dist
end
return 5 * math.rad(pace.GetViewFOV()) / dist
end
end
function mctrl.LinePlaneIntersection(pos, normal, x, y)
local n = normal
local lp = pace.GetViewPos() - pos
local ln = mctrl.ScreenToVec(x, y)
return lp + ln * (-lp:Dot(n) / ln:Dot(n))
end
local function dot2D(x1, y1, x2, y2)
return x1 * x2 + y1 * y2
end
function mctrl.PointToAxis(pos, axis)
local x, y = cursor_pos()
local origin = mctrl.VecToScreen(pos)
local point = mctrl.VecToScreen(pos + axis * 10)
local a = math.atan2(point.y - origin.y, point.x - origin.x)
local d = dot2D(math.cos(a), math.sin(a), point.x - x, point.y - y)
return point.x + math.cos(a) * -d, point.y + math.sin(a) * -d
end
function mctrl.CalculateMovement()
local part = mctrl.GetTarget()
if not part:IsValid() then return end
local axis = mctrl.grab.axis
local offset = mctrl.GetGizmoSize()
local m = mctrl.grab.matrix --part:GetWorldMatrixWithoutOffsets()
local pos = m:GetTranslation()
local forward, right, up = m:GetForward(), -m:GetRight(), m:GetUp()
local world_dir
if axis == "x" then
local localpos = mctrl.LinePlaneIntersection(pos, right, mctrl.PointToAxis(pos, forward))
world_dir = (localpos:Dot(forward) - offset) * forward
elseif axis == "y" then
local localpos = mctrl.LinePlaneIntersection(pos, forward, mctrl.PointToAxis(pos, right))
world_dir = (localpos:Dot(right) - offset) * right
elseif axis == "z" then
local localpos = mctrl.LinePlaneIntersection(pos, forward, mctrl.PointToAxis(pos, up))
world_dir = (localpos:Dot(up) - offset) * up
elseif axis == "view" then
world_dir = mctrl.LinePlaneIntersection(pos, pace.GetViewAngles():Forward(), cursor_pos())
end
if world_dir then
local m2 = Matrix()
m2:SetTranslation(m:GetTranslation() + world_dir)
local wm = mctrl.grab.bone_matrix:GetInverse() * m2
local pos = wm:GetTranslation()
if input.IsKeyDown(KEY_LCONTROL) then
local num = cvar_pos_grid:GetInt("pac_grid_pos_size")
pos.x = math.Round(pos.x / num) * num
pos.y = math.Round(pos.y / num) * num
pos.z = math.Round(pos.z / num) * num
end
pace.Call("VariableChanged", part, "Position", pos, 0.25)
timer.Create("pace_refresh_properties", 0.1, 1, function()
pace.PopulateProperties(part)
end)
end
end
function mctrl.CalculateRotation()
local part = mctrl.GetTarget()
if not part:IsValid() then return end
local axis = mctrl.grab.axis
local ang = mctrl.grab.matrix:GetAngles() --part:GetWorldMatrixWithoutOffsets():GetAngles()
local world_angle
if axis == "x" then
local plane_pos = util.IntersectRayWithPlane(EyePos(), mctrl.ScreenToVec(cursor_pos()), mctrl.grab.matrix:GetTranslation(), mctrl.grab.matrix:GetRight())
if not plane_pos then return end
local diff_angle = (plane_pos - mctrl.grab.matrix:GetTranslation()):Angle()
local local_angle = WorldToLocalAngle(diff_angle, ang)
local p = local_angle.p
if math.abs(local_angle.y) > 90 then
p = -p + 180
end
p = math.NormalizeAngle(p)
world_angle = LocalToWorldAngle(Angle(p, 0, 0), ang)
elseif axis == "y" then
local plane_pos = util.IntersectRayWithPlane(EyePos(), mctrl.ScreenToVec(cursor_pos()), mctrl.grab.matrix:GetTranslation(), mctrl.grab.matrix:GetUp())
if not plane_pos then return end
local diff_angle = (plane_pos - mctrl.grab.matrix:GetTranslation()):Angle()
local local_angle = WorldToLocalAngle(diff_angle, ang)
world_angle = LocalToWorldAngle(Angle(0, local_angle.y - 90, 0), ang)
elseif axis == "z" then
local plane_pos = util.IntersectRayWithPlane(EyePos(), mctrl.ScreenToVec(cursor_pos()), mctrl.grab.matrix:GetTranslation(), mctrl.grab.matrix:GetForward())
if not plane_pos then return end
local diff_angle = (plane_pos - mctrl.grab.matrix:GetTranslation()):Angle()
diff_angle:RotateAroundAxis(mctrl.grab.matrix:GetForward(), 90)
local local_angle = WorldToLocalAngle(diff_angle, ang)
local p = local_angle.p
if local_angle.y > 0 then
p = -p + 180
end
p = math.NormalizeAngle(p)
world_angle = LocalToWorldAngle(Angle(0, 0, p), ang)
end
if world_angle then
local ang = WorldToLocalAngle(world_angle, mctrl.grab.bone_matrix:GetAngles())
if input.IsKeyDown(KEY_LCONTROL) then
local num = cvar_ang_grid:GetInt("pac_grid_ang_size")
ang.p = math.Round(ang.p / num) * num
ang.y = math.Round(ang.y / num) * num
ang.r = math.Round(ang.r / num) * num
end
pace.Call("VariableChanged", part, "Angles", ang, 0.25)
timer.Create("pace_refresh_properties", 0.1, 1, function()
pace.PopulateProperties(part)
end)
end
end
mctrl.grab = {
mode = nil,
axis = nil
}
local GRAB_AND_CLONE = CreateClientConVar("pac_grab_clone", "1", true, false, "Holding shift when moving or rotating a part creates its clone")
function mctrl.GUIMousePressed(mc)
if mc ~= MOUSE_LEFT then return end
local target = mctrl.GetTarget()
if not target:IsValid() then return end
local x, y = input.GetCursorPos()
local pos, ang = mctrl.GetWorldPosition()
if not pos or not ang then return end
local forward, right, up = mctrl.GetAxes(ang)
local r = mctrl.GetGizmoSize()
-- Movement
do
local axis
for i, v in pairs({
x = mctrl.VecToScreen(pos + forward * r),
y = mctrl.VecToScreen(pos + right * r),
z = mctrl.VecToScreen(pos + up * r),
view = mctrl.VecToScreen(pos)
}) do
local d = math.sqrt((v.x - x) ^ 2 + (v.y - y) ^ 2)
if d <= mctrl.grab_dist then
axis = {
axis = i,
pos = v
}
break
end
end
if axis then
mctrl.grab = {}
mctrl.grab.mode = "move"
mctrl.grab.axis = axis.axis
local x, y = input.GetCursorPos()
mctrl.grab.mouse_offset = {
x = math.ceil(axis.pos.x - x),
y = math.ceil(axis.pos.y - y),
}
mctrl.grab.matrix = target:GetWorldMatrixWithoutOffsets() * Matrix()
mctrl.grab.bone_matrix = target:GetBoneMatrix() * Matrix()
if GRAB_AND_CLONE:GetBool() and input.IsShiftDown() then
local copy = target:Clone()
copy:SetParent(copy:GetParent())
end
pace.RecordUndoHistory()
return true
end
end
-- Rotation
do
local axis
for i, v in pairs({
x = mctrl.VecToScreen(pos + forward * r * mctrl.angle_pos),
y = mctrl.VecToScreen(pos + right * r * mctrl.angle_pos),
z = mctrl.VecToScreen(pos + up * r * mctrl.angle_pos)
}) do
local d = math.sqrt((v.x - x) ^ 2 + (v.y - y) ^ 2)
if d <= mctrl.grab_dist then
axis = {
axis = i,
pos = v
}
break
end
end
if axis then
mctrl.grab = {}
mctrl.grab.mode = "rotate"
mctrl.grab.axis = axis.axis
local x, y = input.GetCursorPos()
mctrl.grab.mouse_offset = {
x = math.ceil(axis.pos.x - x) + 0.5,
y = math.ceil(axis.pos.y - y) + 0.5,
}
mctrl.grab.matrix = target:GetWorldMatrixWithoutOffsets() * Matrix()
mctrl.grab.bone_matrix = target:GetBoneMatrix() * Matrix()
mctrl.grab.dist = dist
if GRAB_AND_CLONE:GetBool() and input.IsShiftDown() then
local copy = target:Clone()
copy:SetParent(copy:GetParent())
end
pace.RecordUndoHistory()
return true
end
end
end
function mctrl.GUIMouseReleased(mc)
if mc == MOUSE_LEFT then
mctrl.grab = nil
end
pace.RecordUndoHistory()
end
local white = surface.GetTextureID("gui/center_gradient.vtf")
local function DrawLineEx(x1, y1, x2, y2, w, skip_tex)
w = w or 1
if not skip_tex then
surface.SetTexture(white)
end
local dx, dy = x1 - x2, y1 - y2
local ang = math.atan2(dx, dy)
local dst = math.sqrt((dx * dx) + (dy * dy))
x1 = x1 - dx * 0.5
y1 = y1 - dy * 0.5
surface.DrawTexturedRectRotated(x1, y1, w, dst, math.deg(ang))
end
local function DrawLine(x, y, a, b)
DrawLineEx(x, y, a, b, 3)
end
local function DrawCircleEx(x, y, rad, res, ...)
res = res or 16
local spacing = (res / rad) - 0.1
for i = 0, res do
local i1 = ((i + 0) / res) * math.pi * 2
local i2 = ((i + 1 + spacing) / res) * math.pi * 2
DrawLineEx(x + math.sin(i1) * rad, y + math.cos(i1) * rad, x + math.sin(i2) * rad, y + math.cos(i2) * rad, ...)
end
end
function mctrl.LineToBox(origin, point, siz)
siz = siz or 7
DrawLine(origin.x, origin.y, point.x, point.y)
DrawCircleEx(point.x, point.y, siz, 32, 2)
end
function mctrl.RotationLines(pos, dir, dir2, r)
local pr = mctrl.VecToScreen(pos + dir * r * mctrl.angle_pos)
local pra = mctrl.VecToScreen(pos + dir * r * (mctrl.angle_pos * 0.9) + dir2 * r * 0.08)
local prb = mctrl.VecToScreen(pos + dir * r * (mctrl.angle_pos * 0.9) + dir2 * r * -0.08)
DrawLine(pr.x, pr.y, pra.x, pra.y)
DrawLine(pr.x, pr.y, prb.x, prb.y)
end
function mctrl.HUDPaint()
mctrl.LastThinkCall = FrameNumber()
if pace.IsSelecting then return end
local target = mctrl.GetTarget()
if not target then return end
local pos, ang = mctrl.GetWorldPosition()
if not pos or not ang then return end
local forward, right, up = mctrl.GetAxes(ang)
local radius = mctrl.GetGizmoSize()
local origin = mctrl.VecToScreen(pos)
local forward_point = mctrl.VecToScreen(pos + forward * radius)
local right_point = mctrl.VecToScreen(pos + right * radius)
local up_point = mctrl.VecToScreen(pos + up * radius)
if origin.visible or forward_point.visible or right_point.visible or up_point.visible then
if mctrl.grab and (mctrl.grab.axis == "x" or mctrl.grab.axis == "view") then
surface.SetDrawColor(255, 200, 0, 255)
else
surface.SetDrawColor(255, 80, 80, 255)
end
mctrl.LineToBox(origin, forward_point)
mctrl.RotationLines(pos, forward, up, radius)
if mctrl.grab and (mctrl.grab.axis == "y" or mctrl.grab.axis == "view") then
surface.SetDrawColor(255, 200, 0, 255)
else
surface.SetDrawColor(80, 255, 80, 255)
end
mctrl.LineToBox(origin, right_point)
mctrl.RotationLines(pos, right, forward, radius)
if mctrl.grab and (mctrl.grab.axis == "z" or mctrl.grab.axis == "view") then
surface.SetDrawColor(255, 200, 0, 255)
else
surface.SetDrawColor(80, 80, 255, 255)
end
mctrl.LineToBox(origin, up_point)
mctrl.RotationLines(pos, up, right, radius)
surface.SetDrawColor(255, 200, 0, 255)
DrawCircleEx(origin.x, origin.y, 4, 32, 2)
end
end
function mctrl.Think()
if pace.IsSelecting then return end
if not mctrl.target:IsValid() then return end
if not mctrl.grab then return end
if mctrl.grab.axis and mctrl.grab.mode == "move" then
mctrl.CalculateMovement()
elseif mctrl.grab.axis and mctrl.grab.mode == "rotate" then
mctrl.CalculateRotation()
end
end
pac.AddHook("Think", "pace_mctrl_Think", mctrl.Think)
pace.mctrl = mctrl