From 4d943296772fb873d11103fe11c8c624bccd7b84 Mon Sep 17 00:00:00 2001 From: chanayane Date: Sun, 8 Mar 2026 02:32:26 +0100 Subject: [PATCH] New features : Floater that shows a compass, and let us align our avatar on cardinal points or face towards the nearest avatar, as well as some context menu items in minimap, nearby avatars and avatars to face towards a given avatar --- indra/newview/CMakeLists.txt | 2 + indra/newview/app_settings/commands.xml | 18 + indra/newview/app_settings/settings.xml | 11 + indra/newview/fsfloateravataralign.cpp | 597 ++++++++++++++++++ indra/newview/fsfloateravataralign.h | 135 ++++ indra/newview/fsradarmenu.cpp | 21 + indra/newview/fsradarmenu.h | 2 + indra/newview/llinspectavatar.cpp | 15 + indra/newview/llnetmap.cpp | 27 + indra/newview/llnetmap.h | 4 + indra/newview/llviewerfloaterreg.cpp | 5 + indra/newview/llviewermenu.cpp | 47 ++ .../skins/default/textures/textures.xml | 2 + .../textures/toolbar_icons/compass.png | Bin 0 -> 3618 bytes .../textures/toolbar_icons/facenearest.png | Bin 0 -> 3155 bytes .../default/xui/en/floater_avatar_align.xml | 34 + .../xui/en/floater_avatar_align_mini.xml | 54 ++ .../default/xui/en/menu_attachment_other.xml | 8 + .../default/xui/en/menu_avatar_other.xml | 11 +- .../skins/default/xui/en/menu_fs_radar.xml | 9 + .../skins/default/xui/en/menu_mini_map.xml | 8 + .../xui/en/menu_pie_attachment_other.xml | 12 + .../default/xui/en/menu_pie_avatar_other.xml | 12 + .../newview/skins/default/xui/en/strings.xml | 6 + 24 files changed, 1039 insertions(+), 1 deletion(-) create mode 100644 indra/newview/fsfloateravataralign.cpp create mode 100644 indra/newview/fsfloateravataralign.h create mode 100644 indra/newview/skins/default/textures/toolbar_icons/compass.png create mode 100644 indra/newview/skins/default/textures/toolbar_icons/facenearest.png create mode 100644 indra/newview/skins/default/xui/en/floater_avatar_align.xml create mode 100644 indra/newview/skins/default/xui/en/floater_avatar_align_mini.xml diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index fd08de9bc27..b726da659fd 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -116,6 +116,7 @@ set(viewer_SOURCE_FILES fsfloateraddtocontactset.cpp fsfavoritegroups.cpp fsfloaterassetblacklist.cpp + fsfloateravataralign.cpp fsfloateravatarrendersettings.cpp fsfloaterblocklist.cpp fsfloatercontacts.cpp @@ -982,6 +983,7 @@ set(viewer_HEADER_FILES fsfloaterim.h fsfloaterimcontainer.h fsfloaternearbychat.h + fsfloateravataralign.h fsfloaterpartialinventory.h fsfloaterplacedetails.h fsfloaterposer.h diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml index 490fed10a06..0553f62c6e1 100644 --- a/indra/newview/app_settings/commands.xml +++ b/indra/newview/app_settings/commands.xml @@ -689,4 +689,22 @@ is_running_parameters="omnifilter" checkbox_control="OmnifilterEnabled" /> + + + + + diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index ab8714538a6..e1df4723cef 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -2252,6 +2252,17 @@ Value https://lecs-viewer-web-components.s3.amazonaws.com/v3.0/[GRID_LOWERCASE]/vawp/index.html + AvatarAlignMini + + Comment + Use the compact mini compass floater instead of the full compass. + Persist + 1 + Type + Boolean + Value + 0 + AvatarBakedTextureUploadTimeout diff --git a/indra/newview/fsfloateravataralign.cpp b/indra/newview/fsfloateravataralign.cpp new file mode 100644 index 00000000000..d0efec26058 --- /dev/null +++ b/indra/newview/fsfloateravataralign.cpp @@ -0,0 +1,597 @@ + /** + * @file fsfloateravataralign.cpp + * @brief Floater for rotating the avatar to face cardinal directions or nearest avatar + * @author chanayane@firestorm + * + * $LicenseInfo:firstyear=2026&license=fsviewerlgpl$ + * Phoenix Firestorm Viewer Source Code + * Copyright (C) 2026, Ayane Lyla + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA + * http://www.firestormviewer.org + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "fsfloateravataralign.h" + +#include "llagent.h" +#include "llviewercontrol.h" +#include "llcharacter.h" +#include "llfloaterreg.h" +#include "llfontgl.h" +#include "llrender.h" +#include "llrender2dutils.h" +#include "llvoavatar.h" +#include "llvoavatarself.h" +#include "llviewermessage.h" + +// ============================================================ +// FSAvatarAlignBase +// ============================================================ + +FSAvatarAlignBase::FSAvatarAlignBase(const LLSD& key) + : LLFloater(key) +{ +} + +// static +FSAvatarAlignBase* FSAvatarAlignBase::getActive() +{ + if (gSavedSettings.getBOOL("AvatarAlignMini")) + return LLFloaterReg::getTypedInstance("avatar_align_mini"); + return LLFloaterReg::getTypedInstance("avatar_align"); +} + +void FSAvatarAlignBase::draw() +{ + LLFloater::draw(); + drawCompass(); +} + +FSAvatarAlignBase::CompassLayout FSAvatarAlignBase::buildCompassLayout() const +{ + LLRect local = getLocalRect(); + S32 header_h = getHeaderHeight(); + S32 area_top = local.mTop - header_h - getToolbarHeight(); + S32 area_bottom = local.mBottom + getBottomReserve(); + S32 avail_h = area_top - area_bottom; + S32 avail_w = local.getWidth(); + + // Vertical clearances: top_clear + bot_clear = overhead. + // Full: 27 (N label) + 53 (S label + bearing gap + margin) = 80 + // Mini: 7 (toolbar gap) + 22 (bearing gap + text) = 29 + // R is always computed with the same overhead so it never jumps discontinuously. + const bool mini = isMiniMode(); + S32 top_clear = mini ? 7 : 27; + S32 overhead = mini ? 29 : 80; + // Full mode needs extra horizontal margin so E/W labels don't clip the floater edge + S32 h_margin = mini ? 8 : 28; + S32 R = llmax(llmin(avail_w / 2 - h_margin, (avail_h - overhead) / 2), 30); + + CompassLayout lo; + lo.R = R; + lo.cx = (F32)local.getCenterX(); + lo.cy = (F32)(area_top - top_clear - R); + lo.mini_compact = mini && R < 50; // too small to show bearing/intercardinals + lo.show_labels = !mini && R >= 50; + lo.bearing_y = mini ? ((S32)lo.cy - R - 10) : ((S32)lo.cy - R - 25); + lo.sq_half = mini ? R : (R + 20); + lo.toggle_label = mini ? "Mini" : "Full"; + return lo; +} + +void FSAvatarAlignBase::drawCompass() +{ + const CompassLayout lo = buildCompassLayout(); + + mCompassCX = (S32)lo.cx; + mCompassCY = (S32)lo.cy; + mCompassR = lo.R; + + F32 cx = lo.cx; + F32 cy = lo.cy; + F32 fR = (F32)lo.R; + + gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); + + // Background circle + gGL.color4f(0.08f, 0.08f, 0.10f, 0.90f); + gl_circle_2d(cx, cy, fR, 64, TRUE); + + // Outer ring + gGL.color4f(0.45f, 0.45f, 0.45f, 1.f); + gl_circle_2d(cx, cy, fR, 64, FALSE); + + // Inner ring at half radius + gGL.color4f(0.25f, 0.25f, 0.25f, 1.f); + gl_circle_2d(cx, cy, fR * 0.5f, 48, FALSE); + + // Tick marks at 45° intervals + gGL.begin(LLRender::LINES); + gGL.color4f(0.35f, 0.35f, 0.35f, 1.f); + for (S32 i = 0; i < 8; ++i) + { + F32 a = i * F_PI / 4.f; + F32 sa = sinf(a), ca = cosf(a); + gGL.vertex2f(cx + (fR - 7.f) * sa, cy + (fR - 7.f) * ca); + gGL.vertex2f(cx + fR * sa, cy + fR * ca); + } + gGL.end(); + + // Hover highlight: a semi-transparent light overlay drawn on whichever zone the mouse is over + if (mHoverOctant == -2) + { + // Center zone: fill the inner circle + gGL.color4f(1.f, 1.f, 1.f, 0.18f); + gl_circle_2d(cx, cy, fR * 0.25f, 24, TRUE); + } + else if (mHoverOctant >= 0) + { + // Outer ring: fill the 45° pie slice for the hovered octant. + // The slice is built as a triangle fan between the inner and outer radius. + F32 hoverAngle = mHoverOctant * 45.f * DEG_TO_RAD; // center angle of the slice + F32 halfSpan = F_PI / 8.f; // half of 45° in radians + F32 innerR = fR * 0.25f; // inner boundary (center zone edge) + S32 segs = 8; // subdivisions for a smooth arc + gGL.begin(LLRender::TRIANGLES); + gGL.color4f(1.f, 1.f, 1.f, 0.13f); + for (S32 i = 0; i < segs; ++i) + { + // Two adjacent arc angles for this subdivision + F32 a0 = hoverAngle - halfSpan + (F32)i * (2.f * halfSpan / segs); + F32 a1 = hoverAngle - halfSpan + (F32)(i + 1) * (2.f * halfSpan / segs); + // Two triangles forming a trapezoid between innerR and fR for this strip + gGL.vertex2f(cx + innerR * sinf(a0), cy + innerR * cosf(a0)); + gGL.vertex2f(cx + fR * sinf(a0), cy + fR * cosf(a0)); + gGL.vertex2f(cx + fR * sinf(a1), cy + fR * cosf(a1)); + gGL.vertex2f(cx + innerR * sinf(a0), cy + innerR * cosf(a0)); + gGL.vertex2f(cx + fR * sinf(a1), cy + fR * cosf(a1)); + gGL.vertex2f(cx + innerR * sinf(a1), cy + innerR * cosf(a1)); + } + gGL.end(); + } + + // Draw the 4 cardinal arms as kite (diamond) shapes. + // Each arm points outward from center, colored by convention (red=North, white=South, grey=E/W). + struct ArmDef { F32 angle_deg; F32 r, g, b; }; + static const ArmDef ARMS[] = { + { 0.f, 0.85f, 0.15f, 0.15f }, // North: red + { 180.f, 0.85f, 0.85f, 0.85f }, // South: white + { 90.f, 0.65f, 0.65f, 0.65f }, // East: grey + { 270.f, 0.65f, 0.65f, 0.65f }, // West: grey + }; + + F32 tipR = fR - 4.f; // how far out the arm tip reaches + F32 sideR = fR * 0.245f; // half-width of the arm at its base (center of compass) + + gGL.begin(LLRender::TRIANGLES); + for (const auto& arm : ARMS) + { + F32 a = arm.angle_deg * DEG_TO_RAD; + F32 al = a - F_PI_BY_TWO; // left side angle + F32 ar = a + F_PI_BY_TWO; // right side angle + + // tip point and the two base corners of the kite + F32 tx = cx + tipR * sinf(a); F32 ty = cy + tipR * cosf(a); + F32 lx = cx + sideR * sinf(al); F32 ly = cy + sideR * cosf(al); + F32 rx = cx + sideR * sinf(ar); F32 ry = cy + sideR * cosf(ar); + + // two triangles: tip->center->left, tip->right->center + gGL.color4f(arm.r, arm.g, arm.b, 1.f); + gGL.vertex2f(tx, ty); gGL.vertex2f(cx, cy); gGL.vertex2f(lx, ly); + gGL.vertex2f(tx, ty); gGL.vertex2f(rx, ry); gGL.vertex2f(cx, cy); + } + gGL.end(); + + // Intercardinal arms (NE, SE, SW, NW): same kite shape as cardinals but shorter and narrower. + // Skipped entirely when the floater is in compact mini mode (too small to be legible). + F32 tipR2 = fR * 0.62f; // shorter reach than cardinal arms + F32 sideR2 = fR * 0.10f; // narrower base + static const F32 INTER_ANGLES[] = { 45.f, 135.f, 225.f, 315.f }; + + if (!lo.mini_compact) + { + gGL.begin(LLRender::TRIANGLES); + gGL.color4f(0.55f, 0.55f, 0.55f, 1.f); + for (F32 angle_deg : INTER_ANGLES) + { + F32 a = angle_deg * DEG_TO_RAD; + F32 al = a - F_PI_BY_TWO; + F32 ar = a + F_PI_BY_TWO; + + // tip and base corners, same geometry as cardinal arms + F32 tx = cx + tipR2 * sinf(a); F32 ty = cy + tipR2 * cosf(a); + F32 lx = cx + sideR2 * sinf(al); F32 ly = cy + sideR2 * cosf(al); + F32 rx = cx + sideR2 * sinf(ar); F32 ry = cy + sideR2 * cosf(ar); + + gGL.vertex2f(tx, ty); gGL.vertex2f(cx, cy); gGL.vertex2f(lx, ly); + gGL.vertex2f(tx, ty); gGL.vertex2f(rx, ry); gGL.vertex2f(cx, cy); + } + gGL.end(); + } + + // Center dot + gGL.color4f(0.20f, 0.20f, 0.20f, 1.f); + gl_circle_2d(cx, cy, 5.f, 16, TRUE); + gGL.color4f(0.50f, 0.50f, 0.50f, 1.f); + gl_circle_2d(cx, cy, 5.f, 16, FALSE); + + // Heading needle: a yellow triangle pointing in the direction the agent is currently facing. + LLVector3 at = gAgent.getAtAxis(); + at.mV[VZ] = 0.f; + if (at.normalize() > 0.01f) + { + F32 nl = tipR * 0.88f; // needle tip reaches slightly inside the compass rim + F32 pw = 4.f; // half-width of the needle base + + // Tip of the needle, pointing in the facing direction + F32 nx = cx + nl * at.mV[VX]; + F32 ny = cy + nl * at.mV[VY]; + // Two base corners, perpendicular to the facing direction + F32 bx1 = cx - at.mV[VY] * pw; F32 by1 = cy + at.mV[VX] * pw; + F32 bx2 = cx + at.mV[VY] * pw; F32 by2 = cy - at.mV[VX] * pw; + + gGL.begin(LLRender::TRIANGLES); + gGL.color4f(1.f, 0.85f, 0.f, 0.95f); // yellow + gGL.vertex2f(nx, ny); gGL.vertex2f(bx1, by1); gGL.vertex2f(bx2, by2); + gGL.end(); + } + + // Cardinal labels: only in full mode and when compass is large enough + LLFontGL* font = LLFontGL::getFontSansSerifSmall(); + if (lo.show_labels) + { + S32 ld = lo.R + 10; + LLColor4 col_n(1.f, 0.55f, 0.55f, 1.f); + LLColor4 col_o(0.90f, 0.90f, 0.90f, 1.f); + + font->renderUTF8("N", 0, cx, (F32)(mCompassCY + ld), col_n, LLFontGL::HCENTER, LLFontGL::BOTTOM, LLFontGL::BOLD, LLFontGL::DROP_SHADOW); + font->renderUTF8("S", 0, cx, (F32)(mCompassCY - ld), col_o, LLFontGL::HCENTER, LLFontGL::TOP, LLFontGL::NORMAL, LLFontGL::NO_SHADOW); + font->renderUTF8("E", 0, (F32)(mCompassCX + ld),(F32)mCompassCY, col_o, LLFontGL::LEFT, LLFontGL::VCENTER, LLFontGL::NORMAL, LLFontGL::NO_SHADOW); + font->renderUTF8("W", 0, (F32)(mCompassCX - ld),(F32)mCompassCY, col_o, LLFontGL::RIGHT, LLFontGL::VCENTER, LLFontGL::NORMAL, LLFontGL::NO_SHADOW); + } + + // Bearing label: hidden in mini compact mode to give compass more room + if (!lo.mini_compact) + { + LLVector3 hat = gAgent.getAtAxis(); + hat.mV[VZ] = 0.f; + hat.normalize(); + F32 bearing = fmodf(atan2f(hat.mV[VX], hat.mV[VY]) * RAD_TO_DEG + 360.f, 360.f); + std::string bearing_str = llformat("%03.0f\xC2\xB0", bearing); + font->renderUTF8(bearing_str, 0, cx, (F32)lo.bearing_y, + LLColor4(0.85f, 0.85f, 0.85f, 1.f), LLFontGL::HCENTER, LLFontGL::TOP, + LLFontGL::NORMAL, LLFontGL::NO_SHADOW); + } + + // Toggle-mode button, placed in the top-right corner of the compass bounding square. + // Size is computed from the label text so it fits snugly regardless of font scaling. + const std::string& lbl = lo.toggle_label; + S32 btn_w = (S32)font->getWidth(lbl) + 8; + S32 btn_h = 14; + // In mini mode the square edge is just the ring radius; in full mode it extends further to include the N/E labels. + S32 btn_x = mCompassCX + lo.sq_half - btn_w; + S32 btn_yt = mCompassCY + lo.sq_half; + + mToggleBtnRect.set(btn_x, btn_yt, btn_x + btn_w, btn_yt - btn_h); + + // Dark fill, then a white border that brightens on hover + gGL.color4f(0.f, 0.f, 0.f, 0.55f); + gl_rect_2d(mToggleBtnRect, true); + gGL.color4f(1.f, 1.f, 1.f, mHoverToggle ? 1.f : 0.65f); + gl_rect_2d(mToggleBtnRect, false); + + // Label centered inside the button + font->renderUTF8(lbl, 0, + (F32)(mToggleBtnRect.mLeft + mToggleBtnRect.mRight) * 0.5f, + (F32)(mToggleBtnRect.mBottom + mToggleBtnRect.mTop) * 0.5f, + LLColor4::white, LLFontGL::HCENTER, LLFontGL::VCENTER, + LLFontGL::NORMAL, LLFontGL::NO_SHADOW); +} + +bool FSAvatarAlignBase::handleMouseDown(S32 x, S32 y, MASK mask) +{ + // The toggle button (mini/full mode switch) takes priority over everything else + if (mToggleBtnRect.notEmpty() && mToggleBtnRect.pointInRect(x, y)) + { + onToggleMode(); + return true; + } + + if (mCompassR > 0) + { + // Check if the click landed inside the compass circle + S32 dx = x - mCompassCX; + S32 dy = y - mCompassCY; + F32 dist = sqrtf((F32)(dx * dx + dy * dy)); + + if (dist <= (F32)mCompassR) + { + if (dist < (F32)mCompassR * 0.25f) + { + // Click in the center zone: face the nearest avatar + onClickFaceNearestAvatar(); + } + else + { + // Click in the outer ring: snap to the closest cardinal/intercardinal direction. + // Convert the click angle to degrees (north-up), then round to the nearest 45° octant. + F32 deg = atan2f((F32)dx, (F32)dy) * RAD_TO_DEG; + deg = fmodf(deg + 360.f, 360.f); + F32 octant = fmodf((F32)(ll_round(deg / 45.f)) * 45.f, 360.f); + onClickCardinal(octant); + } + return true; + } + } + return LLFloater::handleMouseDown(x, y, mask); +} + +bool FSAvatarAlignBase::handleHover(S32 x, S32 y, MASK mask) +{ + // Check if the mouse is over the toggle button; reset octant highlight in any case. + mHoverToggle = mToggleBtnRect.notEmpty() && mToggleBtnRect.pointInRect(x, y); + mHoverOctant = -1; // -1 means no compass zone is highlighted + + // Only test compass zones when the toggle button is not already consuming the hover + if (!mHoverToggle && mCompassR > 0) + { + S32 dx = x - mCompassCX; + S32 dy = y - mCompassCY; + F32 dist = sqrtf((F32)(dx * dx + dy * dy)); + if (dist <= (F32)mCompassR) + { + if (dist < (F32)mCompassR * 0.25f) + mHoverOctant = -2; // -2 means center zone (face nearest avatar) + else + { + // Outer ring: determine which of the 8 octants the mouse is in + F32 deg = fmodf(atan2f((F32)dx, (F32)dy) * RAD_TO_DEG + 360.f, 360.f); + mHoverOctant = (S32)(ll_round(deg / 45.f)) % 8; + } + } + } + return LLFloater::handleHover(x, y, mask); +} + +void FSAvatarAlignBase::snapAvatarBody(const LLVector3& target_at) +{ + if (!isAgentAvatarValid() || !gAgentAvatarp->mRoot) + return; + + // Strip any vertical component so the avatar stays upright + LLVector3 at = target_at; + at.mV[VZ] = 0.f; + if (at.normalize() < 0.001f) + return; + + // Build a clean rotation from the forward direction, keeping "up" as the world Z axis + LLVector3 up(0.f, 0.f, 1.f); + LLVector3 left = up % at; + left.normalize(); + at = left % up; + + // Apply the new orientation and plant the avatar at the agent's current position + gAgentAvatarp->mRoot->setWorldRotation(LLQuaternion(at, left, up)); + gAgentAvatarp->mRoot->setWorldPosition(gAgent.getPositionAgent()); +} + +void FSAvatarAlignBase::snapRemoteAvatarBody(LLVOAvatar* avatar) +{ + if (!avatar || avatar->isDead() || !avatar->mRoot) + return; + + // Reset the avatar's skeleton to exactly the server rotation and position + avatar->mRoot->setWorldRotation(avatar->getRotation()); + avatar->mRoot->setWorldPosition(avatar->getPositionAgent()); +} + +void FSAvatarAlignBase::applyRotation(const LLVector3& direction) +{ + // Rotate the agent camera/frame, tell the server, then fix up both avatar bodies visually + gAgent.resetAxes(direction); + send_agent_update(true, false); + snapAvatarBody(direction); + snapRemoteAvatarBody(mTargetAvatar); + mTargetAvatar = nullptr; +} + +void FSAvatarAlignBase::rotateAgentTo(F32 target_deg) +{ + // Convert an absolute compass angle (degrees, north=0) to a world direction and rotate + F32 yaw_rad = target_deg * DEG_TO_RAD; + LLVector3 look_at(sinf(yaw_rad), cosf(yaw_rad), 0.f); + applyRotation(look_at); +} + +void FSAvatarAlignBase::onClickCardinal(F32 target_deg) +{ + // Rotate to the exact compass angle that was clicked on the compass ring + rotateAgentTo(target_deg); +} + +void FSAvatarAlignBase::onClickRotate(F32 delta_deg) +{ + // Rotate by a relative offset from the current facing direction + LLVector3 at = gAgent.getFrameAgent().getAtAxis(); + at.mV[VZ] = 0.f; + at.normalize(); + F32 yaw_deg = atan2f(at.mV[VX], at.mV[VY]) * RAD_TO_DEG; + rotateAgentTo(fmodf(yaw_deg + delta_deg + 360.f, 360.f)); +} + +void FSAvatarAlignBase::onClickNearest() +{ + // Snap the current heading to the closest 45-degree increment + LLVector3 at = gAgent.getFrameAgent().getAtAxis(); + at.mV[VZ] = 0.f; + at.normalize(); + F32 yaw_deg = atan2f(at.mV[VX], at.mV[VY]) * RAD_TO_DEG; + yaw_deg = fmodf(yaw_deg + 360.f, 360.f); + F32 nearest_deg = fmodf((F32)(ll_round(yaw_deg / 45.f) * 45), 360.f); + rotateAgentTo(nearest_deg); +} + +bool FSAvatarAlignBase::isAvatarInRange(LLVOAvatar* avatar) const +{ + if (!avatar || avatar->isDead()) + return false; + return dist_vec(avatar->getPositionAgent(), gAgent.getPositionAgent()) <= MAX_FACE_DISTANCE; +} + +void FSAvatarAlignBase::faceAvatar(LLVOAvatar* avatar) +{ + if (!avatar || !isAgentAvatarValid()) + return; + + mTargetAvatar = avatar; + + // Compute the horizontal direction from us to the target avatar and rotate to face it + LLVector3 direction = avatar->getPositionAgent() - gAgentAvatarp->getPositionAgent(); + direction.mV[VZ] = 0.f; + direction.normalize(); + + applyRotation(direction); +} + +void FSAvatarAlignBase::onClickFaceNearestAvatar() +{ + if (!isAgentAvatarValid()) + return; + + LLVector3 my_pos = gAgent.getPositionAgent(); + LLVOAvatar* nearest = nullptr; + F32 nearest_dist_sq = F32_MAX; + + for (LLCharacter* character : LLCharacter::sInstances) + { + LLVOAvatar* avatar = (LLVOAvatar*)character; + // do not select ourself or dead avatar as the nearest avatar + if (avatar->isDead() || avatar->isControlAvatar() || avatar->isSelf()) + continue; + + // do not select someone that is located further than MAX_FACE_DISTANCE meters from us, in any 3D direction + F32 dist_sq = dist_vec_squared(avatar->getPositionAgent(), my_pos); + if (dist_sq > MAX_FACE_DISTANCE * MAX_FACE_DISTANCE) + continue; + + // if that avatar is nearer the previously selected avatar, select it instead + if (dist_sq < nearest_dist_sq) + { + nearest_dist_sq = dist_sq; + nearest = avatar; + } + } + + if (!nearest) + { + LL_WARNS("AvatarAlign") << "No nearby avatar found to face." << LL_ENDL; + return; + } + + faceAvatar(nearest); +} + +// When switching between full and mini mode : +// if floater is located on the left of the app, grow from the left. Otherwiser grow from the right. +// if floater is located on the bottom of the app, grow from the bottom. Otherwiser grow from the top. +void FSAvatarAlignBase::repositionOnToggle(LLFloater* next, const LLRect& old_rect) +{ + LLRect view = gFloaterView->getRect(); + S32 new_w = next->getRect().getWidth(); + S32 new_h = next->getRect().getHeight(); + bool on_left = (old_rect.getCenterX() < view.getCenterX()); + bool on_bot = (old_rect.getCenterY() < view.getCenterY()); + S32 new_left = on_left ? old_rect.mLeft : (old_rect.mRight - new_w); + S32 new_bot = on_bot ? old_rect.mBottom : (old_rect.mTop - new_h); + next->setOrigin(new_left, new_bot); +} + +// ============================================================ +// FSFloaterAvatarAlign (full mode) +// ============================================================ + +FSFloaterAvatarAlign::FSFloaterAvatarAlign(const LLSD& key) + : FSAvatarAlignBase(key) +{ +} + +bool FSFloaterAvatarAlign::postBuild() +{ + // Wire up all rotation buttons and the face-avatar button + childSetAction("btn_rotate_left_90", [this](void*) { onClickRotate(-90.f); }, this); + childSetAction("btn_rotate_left_45", [this](void*) { onClickRotate(-45.f); }, this); + childSetAction("btn_rotate_right_45", [this](void*) { onClickRotate( 45.f); }, this); + childSetAction("btn_rotate_right_90", [this](void*) { onClickRotate( 90.f); }, this); + childSetAction("btn_rotate_left_10", [this](void*) { onClickRotate(-10.f); }, this); + childSetAction("btn_rotate_left_1", [this](void*) { onClickRotate( -1.f); }, this); + childSetAction("btn_rotate_right_1", [this](void*) { onClickRotate( 1.f); }, this); + childSetAction("btn_rotate_right_10", [this](void*) { onClickRotate( 10.f); }, this); + childSetAction("btn_nearest", [this](void*) { onClickNearest(); }, this); + childSetAction("btn_avatar", [this](void*) { onClickFaceNearestAvatar(); }, this); + + return true; +} + +void FSFloaterAvatarAlign::onOpen(const LLSD& key) +{ +} + +void FSFloaterAvatarAlign::onToggleMode() +{ + // Switch to mini mode: save the preference, close this floater, open the mini one + gSavedSettings.setBOOL("AvatarAlignMini", true); + LLRect rect = getRect(); + closeFloater(false); + FSFloaterAvatarAlignMini* mini = LLFloaterReg::showTypedInstance("avatar_align_mini"); + if (mini) { repositionOnToggle(mini, rect); } +} + +// ============================================================ +// FSFloaterAvatarAlignMini (mini mode) +// ============================================================ + +FSFloaterAvatarAlignMini::FSFloaterAvatarAlignMini(const LLSD& key) + : FSAvatarAlignBase(key) +{ +} + +bool FSFloaterAvatarAlignMini::postBuild() +{ + // Mini mode only has fine-step rotation and the face-avatar button + childSetAction("btn_rotate_left_1", [this](void*) { onClickRotate(-1.f); }, this); + childSetAction("btn_rotate_right_1", [this](void*) { onClickRotate( 1.f); }, this); + childSetAction("btn_avatar", [this](void*) { onClickFaceNearestAvatar(); }, this); + + return true; +} + +void FSFloaterAvatarAlignMini::onOpen(const LLSD& key) +{ +} + +void FSFloaterAvatarAlignMini::onToggleMode() +{ + // Switch to full mode: save the preference, close this floater, open the full one + gSavedSettings.setBOOL("AvatarAlignMini", false); + LLRect rect = getRect(); + closeFloater(false); + FSFloaterAvatarAlign* full = LLFloaterReg::showTypedInstance("avatar_align"); + if (full) { repositionOnToggle(full, rect); } +} diff --git a/indra/newview/fsfloateravataralign.h b/indra/newview/fsfloateravataralign.h new file mode 100644 index 00000000000..7759a8619cb --- /dev/null +++ b/indra/newview/fsfloateravataralign.h @@ -0,0 +1,135 @@ + /** + * @file fsfloateravataralign.h + * @brief Floater for rotating the avatar to face cardinal directions or nearest avatar + * @author chanayane@firestorm + * + * $LicenseInfo:firstyear=2026&license=fsviewerlgpl$ + * Phoenix Firestorm Viewer Source Code + * Copyright (C) 2026, Ayane Lyla + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA + * http://www.firestormviewer.org + * $/LicenseInfo$ + */ + +#ifndef FS_FLOATER_AVATAR_ALIGN_H +#define FS_FLOATER_AVATAR_ALIGN_H + +#include "llfloater.h" + +class LLVOAvatar; + +// Base class: all non-UI logic and compass rendering. +// Subclasses provide the XUI layout and mode-specific behaviour. +class FSAvatarAlignBase : public LLFloater +{ + LOG_CLASS(FSAvatarAlignBase); +protected: + FSAvatarAlignBase(const LLSD& key); +public: + void draw() override; + bool handleMouseDown(S32 x, S32 y, MASK mask) override; + bool handleHover(S32 x, S32 y, MASK mask) override; + + // Face a specific avatar. Safe to call with nullptr (no-op). + void faceAvatar(LLVOAvatar* avatar); + // Face the nearest non-flying avatar within MAX_FACE_DISTANCE. + void onClickFaceNearestAvatar(); + // Returns true if avatar is alive and within MAX_FACE_DISTANCE. + bool isAvatarInRange(LLVOAvatar* avatar) const; + + // Returns whichever mode's floater instance exists (based on AvatarAlignMini setting). + static FSAvatarAlignBase* getActive(); + + static constexpr F32 MAX_FACE_DISTANCE = 20.f; + +protected: + virtual bool isMiniMode() const { return false; } + virtual S32 getToolbarHeight() const { return 0; } + virtual S32 getBottomReserve() const { return 104; } + virtual void onToggleMode() = 0; + + // Repositions 'next' floater relative to this floater's pre-close rect. + void repositionOnToggle(LLFloater* next, const LLRect& old_rect); + + void onClickCardinal(F32 target_deg); + void onClickRotate(F32 delta_deg); + void onClickNearest(); + void rotateAgentTo(F32 target_deg); + void applyRotation(const LLVector3& direction); + void snapAvatarBody(const LLVector3& target_at); + void snapRemoteAvatarBody(LLVOAvatar* avatar); + // All mode-dependent layout values needed to draw the compass, computed once per frame. + struct CompassLayout + { + S32 R; // compass radius in pixels + F32 cx, cy; // compass center in local screen coords + bool mini_compact; // true when mini mode and radius is too small for bearing/intercardinals + bool show_labels; // true when full mode and radius is large enough for N/S/E/W labels + S32 bearing_y; // Y position of the bearing text below the ring + S32 sq_half; // half-size of the bounding square (used for toggle button placement) + std::string toggle_label; // "Mini" or "Full" + }; + + CompassLayout buildCompassLayout() const; + void drawCompass(); + + LLVOAvatar* mTargetAvatar = nullptr; + + // Compass geometry (local screen coords), updated each draw(). + S32 mCompassCX = 0; + S32 mCompassCY = 0; + S32 mCompassR = 0; + // Hover: -1=none, -2=centre, 0-7=octant (index × 45° = degrees from North CW) + S32 mHoverOctant = -1; + bool mHoverToggle = false; + // Toggle-mode button rect (local coords), updated each draw(). + LLRect mToggleBtnRect; +}; + +// Full-mode floater: complete button set, large compass. +class FSFloaterAvatarAlign : public FSAvatarAlignBase +{ + LOG_CLASS(FSFloaterAvatarAlign); + friend class LLFloaterReg; +private: + FSFloaterAvatarAlign(const LLSD& key); +public: + bool postBuild() override; + void onOpen(const LLSD& key) override; +protected: + void onToggleMode() override; +}; + +// Mini-mode floater: compact toolbar, fill-to-size compass. +class FSFloaterAvatarAlignMini : public FSAvatarAlignBase +{ + LOG_CLASS(FSFloaterAvatarAlignMini); + friend class LLFloaterReg; +private: + FSFloaterAvatarAlignMini(const LLSD& key); +public: + bool postBuild() override; + void onOpen(const LLSD& key) override; +protected: + bool isMiniMode() const override { return true; } + void onToggleMode() override; + S32 getToolbarHeight() const override { return 22; } + S32 getBottomReserve() const override { return 3; } +}; + +#endif // FS_FLOATER_AVATAR_ALIGN_H diff --git a/indra/newview/fsradarmenu.cpp b/indra/newview/fsradarmenu.cpp index 599cb2acc71..895677ad289 100644 --- a/indra/newview/fsradarmenu.cpp +++ b/indra/newview/fsradarmenu.cpp @@ -39,6 +39,8 @@ #include "llcallingcard.h" // for LLAvatarTracker #include "lllogchat.h" #include "llnetmap.h" +#include "fsfloateravataralign.h" +#include "llfloaterreg.h" #include "llviewermenu.h" // for gMenuHolder #include "rlvactions.h" #include "rlvhandler.h" @@ -87,6 +89,7 @@ LLContextMenu* FSRadarMenu::createMenu() registrar.add("Avatar.Calllog", boost::bind(&LLAvatarActions::viewChatHistory, id)); registrar.add("Nearby.People.TeleportToAvatar", boost::bind(&FSRadarMenu::teleportToAvatar, this)); registrar.add("Nearby.People.TrackAvatar", boost::bind(&FSRadarMenu::onTrackAvatarMenuItemClick, this)); + registrar.add("Nearby.People.FaceTowardsAvatar", boost::bind(&FSRadarMenu::onFaceTowardsAvatarMenuItemClick, this)); registrar.add("Nearby.People.SetRenderMode", boost::bind(&FSRadarMenu::onSetRenderMode, this, _2)); registrar.add("Nearby.People.SetAvatarMarkColor", boost::bind(&LLNetMap::setAvatarMarkColor, id, _2)); registrar.add("Nearby.People.ClearAvatarMarkColor", boost::bind(&LLNetMap::clearAvatarMarkColor, id)); @@ -98,6 +101,7 @@ LLContextMenu* FSRadarMenu::createMenu() enable_registrar.add("Avatar.VisibleFreezeEject", boost::bind(&LLAvatarActions::canLandFreezeOrEject, id)); enable_registrar.add("Avatar.VisibleKickTeleportHome", boost::bind(&LLAvatarActions::canEstateKickOrTeleportHome, id)); enable_registrar.add("Nearby.People.CheckRenderMode", boost::bind(&FSRadarMenu::checkSetRenderMode, this, _2)); + enable_registrar.add("Nearby.People.CanFaceTowardsAvatar", boost::bind(&FSRadarMenu::canFaceTowardsAvatar, this)); // create the context menu from the XUI return createFromFile("menu_fs_radar.xml"); @@ -276,6 +280,23 @@ void FSRadarMenu::onTrackAvatarMenuItemClick() LLAvatarActions::track(mUUIDs.front()); } +void FSRadarMenu::onFaceTowardsAvatarMenuItemClick() +{ + LLVOAvatar* avatar = dynamic_cast(gObjectList.findObject(mUUIDs.front())); + FSAvatarAlignBase* floater = FSAvatarAlignBase::getActive(); + if (floater && avatar) + { + floater->faceAvatar(avatar); + } +} + +bool FSRadarMenu::canFaceTowardsAvatar() +{ + LLVOAvatar* avatar = dynamic_cast(gObjectList.findObject(mUUIDs.front())); + return avatar && !avatar->isDead() && + dist_vec(avatar->getPositionAgent(), gAgent.getPositionAgent()) <= FSAvatarAlignBase::MAX_FACE_DISTANCE; +} + void FSRadarMenu::addToContactSet() { LLAvatarActions::addToContactSet(mUUIDs); diff --git a/indra/newview/fsradarmenu.h b/indra/newview/fsradarmenu.h index c4a2e232005..c3b42ef6c74 100644 --- a/indra/newview/fsradarmenu.h +++ b/indra/newview/fsradarmenu.h @@ -47,6 +47,8 @@ class FSRadarMenu : public LLListContextMenu void offerTeleport(); void teleportToAvatar(); void onTrackAvatarMenuItemClick(); + void onFaceTowardsAvatarMenuItemClick(); + bool canFaceTowardsAvatar(); void addToContactSet(); void onSetRenderMode(const LLSD& userdata); bool checkSetRenderMode(const LLSD& userdata); diff --git a/indra/newview/llinspectavatar.cpp b/indra/newview/llinspectavatar.cpp index 6b17662c684..d6543501006 100644 --- a/indra/newview/llinspectavatar.cpp +++ b/indra/newview/llinspectavatar.cpp @@ -46,6 +46,9 @@ #include "llfloaterreg.h" #include "lltextbox.h" #include "lltrans.h" +#include "fsfloateravataralign.h" // Compass floater +#include "llviewerobjectlist.h" // Compass floater +#include "llvoavatar.h" // Compass floater // Undo CHUI-90 and make avatar inspector useful again #include "llagentdata.h" @@ -134,6 +137,7 @@ class LLInspectAvatar : public LLInspect, LLTransientFloater void onClickTeleport(); void onClickTeleportRequest(); void onClickInviteToGroup(); + void onClickFaceTowards(); // Compass floater void onClickPay(); void onClickShare(); void onToggleMute(); @@ -250,6 +254,7 @@ LLInspectAvatar::LLInspectAvatar(const LLSD& sd) mCommitCallbackRegistrar.add("InspectAvatar.Teleport", boost::bind(&LLInspectAvatar::onClickTeleport, this)); mCommitCallbackRegistrar.add("InspectAvatar.TeleportRequest", boost::bind(&LLInspectAvatar::onClickTeleportRequest, this)); mCommitCallbackRegistrar.add("InspectAvatar.InviteToGroup", boost::bind(&LLInspectAvatar::onClickInviteToGroup, this)); + mCommitCallbackRegistrar.add("InspectAvatar.FaceTowards", boost::bind(&LLInspectAvatar::onClickFaceTowards, this)); // Compass floater mCommitCallbackRegistrar.add("InspectAvatar.Pay", boost::bind(&LLInspectAvatar::onClickPay, this)); mCommitCallbackRegistrar.add("InspectAvatar.Share", boost::bind(&LLInspectAvatar::onClickShare, this)); mCommitCallbackRegistrar.add("InspectAvatar.ToggleMute", boost::bind(&LLInspectAvatar::onToggleMute, this)); @@ -729,6 +734,16 @@ void LLInspectAvatar::onClickInviteToGroup() closeFloater(); } +// Compass floater +void LLInspectAvatar::onClickFaceTowards() +{ + LLVOAvatar* avatar = dynamic_cast(gObjectList.findObject(mAvatarID)); + if (!avatar) return; + FSAvatarAlignBase* f = FSAvatarAlignBase::getActive(); + if (f) f->faceAvatar(avatar); +} +// + void LLInspectAvatar::onClickPay() { LLAvatarActions::pay(mAvatarID); diff --git a/indra/newview/llnetmap.cpp b/indra/newview/llnetmap.cpp index 13370a4cca5..796650414dd 100644 --- a/indra/newview/llnetmap.cpp +++ b/indra/newview/llnetmap.cpp @@ -34,6 +34,7 @@ #include "llavatarnamecache.h" #include "llmath.h" #include "llfloaterreg.h" +#include "fsfloateravataralign.h" // Compass floater #include "llfocusmgr.h" #include "lllocalcliprect.h" #include "llrender.h" @@ -212,6 +213,10 @@ bool LLNetMap::postBuild() commitRegistrar.add("Minimap.ClearMarks", boost::bind(&LLNetMap::handleClearMarks, this)); // commitRegistrar.add("Minimap.Cam", boost::bind(&LLNetMap::handleCam, this)); +// Compass floater + commitRegistrar.add("Minimap.FaceTowards", boost::bind(&LLNetMap::handleFaceTowards, this)); + enableRegistrar.add("Minimap.CanFaceTowards", boost::bind(&LLNetMap::canFaceTowards, this)); +// commitRegistrar.add("Minimap.StartTracking", boost::bind(&LLNetMap::handleStartTracking, this)); // [SL:KB] - Patch: World-MiniMap | Checked: 2012-07-08 (Catznip-3.3) commitRegistrar.add("Minimap.ShowProfile", boost::bind(&LLNetMap::handleShowProfile, this, _2)); @@ -2047,6 +2052,28 @@ void LLNetMap::handleCam() } } +// Compass floater +void LLNetMap::handleFaceTowards() +{ + LLVOAvatar* avatar = dynamic_cast(gObjectList.findObject(mClosestAgentRightClick)); + if (!avatar) + return; + // Get-or-create the floater instance; no need to show it just to rotate. + FSAvatarAlignBase* floater = FSAvatarAlignBase::getActive(); + if (floater) + { + floater->faceAvatar(avatar); + } +} + +bool LLNetMap::canFaceTowards() +{ + LLVOAvatar* avatar = dynamic_cast(gObjectList.findObject(mClosestAgentRightClick)); + return avatar && !avatar->isSelf() && !avatar->isDead() && + dist_vec(avatar->getPositionAgent(), gAgent.getPositionAgent()) <= FSAvatarAlignBase::MAX_FACE_DISTANCE; +} +// + // Avatar tracking feature void LLNetMap::handleStartTracking() { diff --git a/indra/newview/llnetmap.h b/indra/newview/llnetmap.h index ea444303181..abadf80b85f 100644 --- a/indra/newview/llnetmap.h +++ b/indra/newview/llnetmap.h @@ -222,6 +222,10 @@ class LLNetMap : public LLUICtrl void handleClearMark(); void handleClearMarks(); void handleCam(); +// Compass floater + void handleFaceTowards(); + bool canFaceTowards(); +// // [SL:KB] - Patch: World-MiniMap | Checked: 2012-07-08 (Catznip-3.3) void handleOverlayToggle(const LLSD& sdParam); void setAvatarProfileLabel(const LLUUID& av_id, const LLAvatarName& avName, const std::string& item_name); diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index dce9f23bedc..563a12e7963 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -242,6 +242,7 @@ #include "vjfloaterlocalmesh.h" // local mesh #include "fsfloaterwhitelisthelper.h" // fs whitelist helper #include "omnifilter.h" // Omnifilter support +#include "fsfloateravataralign.h" // Compass floater // handle secondlife:///app/openfloater/{NAME} URLs const std::string FLOATER_PROFILE("profile"); @@ -687,6 +688,10 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("vram_usage", "floater_fs_vram_usage.xml", static_cast(&LLFloaterReg::build)); LLFloaterReg::add("local_mesh_floater", "floater_vj_local_mesh.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); // local mesh LLFloaterReg::add("fs_whitelist_floater", "floater_whitelist.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); // white list advisor + // Compass floater + LLFloaterReg::add("avatar_align", "floater_avatar_align.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + LLFloaterReg::add("avatar_align_mini", "floater_avatar_align_mini.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + // LLFloaterReg::registerControlVariables(); // Make sure visibility and rect controls get preserved when saving } diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index c864eb739de..0267130c708 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -161,6 +161,7 @@ // [/RLVa:KB] // Firestorm includes +#include "fsfloateravataralign.h" // Compass floater #include "fsassetblacklist.h" #include "fsdata.h" #include "fslslbridge.h" @@ -8167,6 +8168,30 @@ class LLAvatarInviteToGroup : public view_listener_t } }; +// Compass floater - face towards avatar from context menu +class LLAvatarFaceTowards : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + LLVOAvatar* avatar = find_avatar_from_object(LLSelectMgr::getInstance()->getSelection()->getPrimaryObject()); + if (!avatar) return true; + FSAvatarAlignBase* f = FSAvatarAlignBase::getActive(); + if (f) f->faceAvatar(avatar); + return true; + } +}; + +class LLAvatarCanFaceTowards : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + LLVOAvatar* avatar = find_avatar_from_object(LLSelectMgr::getInstance()->getSelection()->getPrimaryObject()); + return avatar && !avatar->isSelf() && !avatar->isDead() && + dist_vec(avatar->getPositionAgent(), gAgent.getPositionAgent()) <= FSAvatarAlignBase::MAX_FACE_DISTANCE; + } +}; +// + class LLAvatarAddFriend : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -13120,6 +13145,10 @@ void initialize_menus() view_listener_t::addMenu(new LLAvatarDebug(), "Avatar.Debug"); view_listener_t::addMenu(new LLAvatarVisibleDebug(), "Avatar.VisibleDebug"); view_listener_t::addMenu(new LLAvatarInviteToGroup(), "Avatar.InviteToGroup"); +// Compass floater - face towards avatar from context menu + view_listener_t::addMenu(new LLAvatarFaceTowards(), "Avatar.FaceTowards"); + view_listener_t::addMenu(new LLAvatarCanFaceTowards(), "Avatar.CanFaceTowards"); +// // FIRE-13515: Re-add give calling card view_listener_t::addMenu(new LLAvatarGiveCard(), "Avatar.GiveCard"); // FIRE-13515: Re-add give calling card @@ -13148,6 +13177,24 @@ void initialize_menus() commit.add("Avatar.OpenMarketplace", boost::bind(&LLWeb::loadURLExternal, gSavedSettings.getString("MarketplaceURL"))); +// Compass floater + commit.add("Avatar.AlignToggle", [](LLUICtrl*, const LLSD&) { + if (gSavedSettings.getBOOL("AvatarAlignMini")) + LLFloaterReg::toggleInstance("avatar_align_mini"); + else + LLFloaterReg::toggleInstance("avatar_align"); + }); + enable.add("Avatar.AlignIsOpen", [](LLUICtrl*, const LLSD&) -> bool { + return gSavedSettings.getBOOL("AvatarAlignMini") + ? LLFloaterReg::instanceVisible("avatar_align_mini", LLSD()) + : LLFloaterReg::instanceVisible("avatar_align", LLSD()); + }); + commit.add("Avatar.FaceNearest", [](LLUICtrl*, const LLSD&) { + FSAvatarAlignBase* f = FSAvatarAlignBase::getActive(); + if (f) f->onClickFaceNearestAvatar(); + }); +// + view_listener_t::addMenu(new LLAvatarEnableAddFriend(), "Avatar.EnableAddFriend"); enable.add("Avatar.EnableFreezeEject", boost::bind(&enable_freeze_eject, _2)); diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml index 19cdcf9770a..ca5d25f54c2 100644 --- a/indra/newview/skins/default/textures/textures.xml +++ b/indra/newview/skins/default/textures/textures.xml @@ -175,6 +175,8 @@ with the same filename but different name + + diff --git a/indra/newview/skins/default/textures/toolbar_icons/compass.png b/indra/newview/skins/default/textures/toolbar_icons/compass.png new file mode 100644 index 0000000000000000000000000000000000000000..f97ec94c0caeca70a4b503e96a6b763eecb9a4c2 GIT binary patch literal 3618 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 literal 0 HcmV?d00001 diff --git a/indra/newview/skins/default/textures/toolbar_icons/facenearest.png b/indra/newview/skins/default/textures/toolbar_icons/facenearest.png new file mode 100644 index 0000000000000000000000000000000000000000..1f6de8f6543b53a41d0ba041fc0c6cadadcca8bc GIT binary patch literal 3155 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 literal 0 HcmV?d00001 diff --git a/indra/newview/skins/default/xui/en/floater_avatar_align.xml b/indra/newview/skins/default/xui/en/floater_avatar_align.xml new file mode 100644 index 00000000000..6db9a37611b --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_avatar_align.xml @@ -0,0 +1,34 @@ + + +