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 00000000000..f97ec94c0ca Binary files /dev/null and b/indra/newview/skins/default/textures/toolbar_icons/compass.png differ 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 00000000000..1f6de8f6543 Binary files /dev/null and b/indra/newview/skins/default/textures/toolbar_icons/facenearest.png differ 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 @@ + + +