From b788f6837e3723692c7cb07936c80bfca707241c Mon Sep 17 00:00:00 2001 From: Adversarius Date: Fri, 24 Jul 2026 00:49:25 -0400 Subject: [PATCH 1/2] mirror tools --- lua/pac3/editor/client/tools.lua | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lua/pac3/editor/client/tools.lua b/lua/pac3/editor/client/tools.lua index 2ddc2f2a1..1f7d765c7 100644 --- a/lua/pac3/editor/client/tools.lua +++ b/lua/pac3/editor/client/tools.lua @@ -863,3 +863,61 @@ pace.AddTool(L"copy from faceposer tool", function(part, suboption) end end end) + +local function mirror_vector(vec, plane) -- plane must be normal vector + return vec - 2 * vec:Dot(plane) * plane +end + +local function mirror_parts(part, plane) + if part.SetPosition then + part:SetPosition(mirror_vector(part:GetPosition(), plane)) + part:SetPositionOffset(mirror_vector(part:GetPositionOffset(), plane)) + end + + if part.SetAngles then + local ang, angO = part:GetAngles(), part:GetAngleOffset() + local fwd, up, fwdO, upO = ang:Forward(), ang:Up(), angO:Forward(), angO:Up() + + part:SetAngles(mirror_vector(fwd, plane):AngleEx(mirror_vector(up, plane))) + part:SetAngleOffset(mirror_vector(fwdO, plane):AngleEx(mirror_vector(upO, plane))) + end + + if part.SetScale then + part:SetScale(mirror_vector(part:GetScale(), plane)) + end + + if part.SetInvert then + part:SetInvert(not part:GetInvert()) + end + + for _, part in ipairs(part:GetChildren()) do + mirror_parts(part, plane) + end +end + +local axis_planes = { + ["x"] = Vector(1, 0, 0), + ["y"] = Vector(0, 1, 0), + ["z"] = Vector(0, 0, 1)} + +pace.AddTool(L"mirror this and children", function(part) + Derma_StringRequest(L"plane", L"input the plane to mirror across", "y", function(plane) + if plane and part:IsValid() then + local axis_plane = axis_planes[string.lower(plane)] + if axis_plane then + mirror_parts(part, axis_plane) + end + end + end) +end) + +pace.AddTool(L"clone and mirror this and children", function(part) + Derma_StringRequest(L"plane", L"input the plane to mirror across", "y", function(plane) + if plane and part:IsValid() then + local axis_plane = axis_planes[string.lower(plane)] + if axis_plane then + mirror_parts(part:Clone(), axis_plane) + end + end + end) +end) From 607c36551a558aa112529d5d227f5173beea7f2c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 28 Jul 2026 21:49:36 -0400 Subject: [PATCH 2/2] improve mirror tool usability I want it accessible on quicksetups and eventually in the gizmo made the function now more flexible for this reason fixed the one case where Scale isn't a vector (faceposer class) added some further instructions as tooltips added a mechanism to override the gizmo's drawn position to preview the real mirror planes --- lua/pac3/editor/client/mctrl.lua | 26 ++++++++++++++++++++++++++ lua/pac3/editor/client/parts.lua | 23 +++++++++++++++++++++++ lua/pac3/editor/client/tools.lua | 11 ++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/mctrl.lua b/lua/pac3/editor/client/mctrl.lua index cd83013b8..aaeba06c2 100644 --- a/lua/pac3/editor/client/mctrl.lua +++ b/lua/pac3/editor/client/mctrl.lua @@ -496,6 +496,31 @@ function mctrl.RotationLines(pos, dir, dir2, r) DrawLine(pr.x, pr.y, prb.x, prb.y) end +function mctrl.ManualPaint(pos, ang, radius) + local forward, right, up = pace.mctrl.GetAxes(ang) + local radius = 2 * pace.mctrl.GetGizmoSize() + local origin = pace.mctrl.VecToScreen(pos) + local forward_point = pace.mctrl.VecToScreen(pos + forward * radius) + local right_point = pace.mctrl.VecToScreen(pos + right * radius) + local up_point = pace.mctrl.VecToScreen(pos + up * radius) + + mctrl.nodraw = true + surface.SetDrawColor(255, 80, 80, 255) + mctrl.LineToBox(origin, forward_point) + mctrl.RotationLines(pos, forward, up, radius) + + surface.SetDrawColor(80, 255, 80, 255) + mctrl.LineToBox(origin, right_point) + mctrl.RotationLines(pos, right, forward, radius) + + surface.SetDrawColor(80, 80, 255, 255) + 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 + function mctrl.HUDPaint() mctrl.LastThinkCall = FrameNumber() if pace.IsSelecting then return end @@ -503,6 +528,7 @@ function mctrl.HUDPaint() if not target then return end local pos, ang = mctrl.GetWorldPosition() if not pos or not ang then return end + if mctrl.nodraw then return end --to allow overridden rendering local forward, right, up = mctrl.GetAxes(ang) local radius = mctrl.GetGizmoSize() local origin = mctrl.VecToScreen(pos) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 8068a4473..510709e1c 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2707,6 +2707,29 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) substitutes:AddOption("interpolator", function() pace.SubstituteBaseMovable(obj, "create_parent", "interpolated_multibone") end):SetIcon("icon16/table_multiple.png") + + --mirroring + local tooltip = "Axis is relative to parent bone\nThe output stays within that same bone\n\nIf you wish to mirror on flipped bones, it can work. I recommend using the bone 'switch sides' action first, and then double-check your axis to make sure you know what's the new bone's actual base angle." + local mirror_submenu1, pnlsubmenu = main:AddSubMenu("Mirror parts...") pnlsubmenu:SetImage("icon16/shape_flip_horizontal.png") pnlsubmenu:SetTooltip(tooltip) + mirror_submenu1:AddOption("x", function() pac.MirrorParts(pace.current_part, "x") end) + mirror_submenu1:AddOption("y", function() pac.MirrorParts(pace.current_part, "y") end) + mirror_submenu1:AddOption("z", function() pac.MirrorParts(pace.current_part, "z") end) + local mirror_submenu2, pnlsubmenu = main:AddSubMenu("Clone and Mirror parts...") pnlsubmenu:SetImage("icon16/shape_flip_horizontal.png") pnlsubmenu:SetTooltip(tooltip) + mirror_submenu2:AddOption("x", function() pac.MirrorParts(pace.current_part:Clone(), "x") end) + mirror_submenu2:AddOption("y", function() pac.MirrorParts(pace.current_part:Clone(), "y") end) + mirror_submenu2:AddOption("z", function() pac.MirrorParts(pace.current_part:Clone(), "z") end) + --override the gizmo to real coordinates used by mirror tool + pac.AddHook("HUDPaint", "mirror_tool_preview_bone_posang", function() + if not IsValid(menu) then + pace.mctrl.nodraw = false pac.RemoveHook("HUDPaint", "mirror_tool_preview_bone_posang") return + end + if pnlsubmenu1:IsHovered() or pnlsubmenu1:IsChildHovered() or pnlsubmenu2:IsHovered() or pnlsubmenu2:IsChildHovered() then + local pos, ang = pac.GetBonePosAng(pace.current_part:GetParentOwner(), pace.current_part.Bone) + pace.mctrl.ManualPaint(pos, ang, 2 * pace.mctrl.GetGizmoSize()) + else + pace.mctrl.nodraw = false + end + end) end local function install_submaterial_options(menu) diff --git a/lua/pac3/editor/client/tools.lua b/lua/pac3/editor/client/tools.lua index 1f7d765c7..8acc042fc 100644 --- a/lua/pac3/editor/client/tools.lua +++ b/lua/pac3/editor/client/tools.lua @@ -882,7 +882,7 @@ local function mirror_parts(part, plane) part:SetAngleOffset(mirror_vector(fwdO, plane):AngleEx(mirror_vector(upO, plane))) end - if part.SetScale then + if part.SetScale and part.ClassName ~= "faceposer" then part:SetScale(mirror_vector(part:GetScale(), plane)) end @@ -900,6 +900,15 @@ local axis_planes = { ["y"] = Vector(0, 1, 0), ["z"] = Vector(0, 0, 1)} +function pac.MirrorParts(part, plane) + if isstring(plane) then + plane = axis_planes[plane] + if not isvector(plane) then return end + end + mirror_parts(part, plane) +end + + pace.AddTool(L"mirror this and children", function(part) Derma_StringRequest(L"plane", L"input the plane to mirror across", "y", function(plane) if plane and part:IsValid() then