From 00ed27bf145e63e019eb45c7ed47d06b2624378f Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sat, 2 Aug 2025 00:53:06 +0100 Subject: [PATCH] FIRE-35769: Add 'World lock joints' option --- indra/newview/fsfloaterposer.cpp | 51 +++++-- indra/newview/fsfloaterposer.h | 2 + indra/newview/fsjointpose.cpp | 22 ++- indra/newview/fsjointpose.h | 35 +++-- indra/newview/fsposeranimator.cpp | 143 +++++++++++++++++- indra/newview/fsposeranimator.h | 47 ++++++ .../skins/default/xui/en/floater_fs_poser.xml | 16 +- 7 files changed, 287 insertions(+), 29 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index bcfd183e8b..d49b9e6a2e 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -192,6 +192,8 @@ bool FSFloaterPoser::postBuild() mFlipJointBtn = getChild("FlipJoint_avatar"); mRecaptureBtn = getChild("button_RecaptureParts"); mTogglePosingBonesBtn = getChild("toggle_PosingSelectedBones"); + mToggleLockWorldRotBtn = getChild("toggle_LockWorldRotation"); + mToggleLockWorldRotBtn->setClickedCallback([this](LLUICtrl*, const LLSD&) { onClickLockWorldRotBtn(); }); mToggleMirrorRotationBtn = getChild("button_toggleMirrorRotation"); mToggleSympatheticRotationBtn = getChild("button_toggleSympatheticRotation"); @@ -517,6 +519,7 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi { std::string bone_name = pj.jointName(); bool posingThisJoint = mPoserAnimator.isPosingAvatarJoint(avatar, pj); + bool jointRotLocked = mPoserAnimator.getRotationIsWorldLocked(avatar, pj); record[bone_name] = bone_name; record[bone_name]["enabled"] = posingThisJoint; @@ -532,9 +535,10 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi continue; record[bone_name]["jointBaseRotationIsZero"] = baseRotationIsZero; - record[bone_name]["rotation"] = rotation.getValue(); - record[bone_name]["position"] = position.getValue(); - record[bone_name]["scale"] = scale.getValue(); + record[bone_name]["rotation"] = rotation.getValue(); + record[bone_name]["position"] = position.getValue(); + record[bone_name]["scale"] = scale.getValue(); + record[bone_name]["worldLocked"] = jointRotLocked; } std::string fullSavePath = @@ -1041,6 +1045,7 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose LLQuaternion quat; bool enabled; bool setJointBaseRotationToZero; + bool worldLocked; S32 version = 0; bool startFromZeroRot = true; @@ -1116,6 +1121,9 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose vec3.setValue(control_map["scale"]); mPoserAnimator.loadJointScale(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3); } + + worldLocked = control_map.has("worldLocked") ? control_map["worldLocked"].asBoolean() : false; + mPoserAnimator.setRotationIsWorldLocked(avatar, *poserJoint, worldLocked); } } } @@ -1449,8 +1457,7 @@ void FSFloaterPoser::onResetJoint(const LLSD data) refreshScaleSlidersAndSpinners(); refreshTrackpadCursor(); enableOrDisableRedoAndUndoButton(); - if (getSavingToBvh()) - refreshTextHighlightingOnJointScrollLists(); + refreshTextHighlightingOnJointScrollLists(); } void FSFloaterPoser::onRedoLastChange() @@ -2373,10 +2380,7 @@ void FSFloaterPoser::addBoldToScrollList(LLScrollListCtrl* list, LLVOAvatar* ava if (!poserJoint) continue; - if (considerExternalFormatSaving) - ((LLScrollListText*)listItem->getColumn(COL_ICON))->setValue(getScrollListIconForJoint(avatar, *poserJoint)); - else - ((LLScrollListText*)listItem->getColumn(COL_ICON))->setValue(""); + ((LLScrollListText*)listItem->getColumn(COL_ICON))->setValue(getScrollListIconForJoint(avatar, *poserJoint)); if (mPoserAnimator.isPosingAvatarJoint(avatar, *poserJoint)) ((LLScrollListText *) listItem->getColumn(COL_NAME))->setFontStyle(LLFontGL::BOLD); @@ -2393,6 +2397,9 @@ std::string FSFloaterPoser::getScrollListIconForJoint(LLVOAvatar* avatar, FSPose if (mPoserAnimator.getRotationIsWorldLocked(avatar, joint)) return tryGetString("icon_rotation_is_world_locked"); + if (!getSavingToBvh()) + return ""; + if (joint.boneType() == COL_VOLUMES) return tryGetString("icon_rotation_does_not_export"); @@ -2678,3 +2685,29 @@ bool FSFloaterPoser::getSavingToBvh() } void FSFloaterPoser::onClickSavingToBvh() { refreshTextHighlightingOnJointScrollLists(); } + +void FSFloaterPoser::onClickLockWorldRotBtn() +{ + auto selectedJoints = getUiSelectedPoserJoints(); + if (selectedJoints.size() < 1) + return; + + LLVOAvatar* avatar = getUiSelectedAvatar(); + if (!avatar) + return; + + if (!mPoserAnimator.isPosingAvatar(avatar)) + return; + + for (auto item : selectedJoints) + { + bool currentlyPosingJoint = mPoserAnimator.isPosingAvatarJoint(avatar, *item); + if (!currentlyPosingJoint) + continue; + + bool newLockState = !mPoserAnimator.getRotationIsWorldLocked(avatar, *item); + mPoserAnimator.setRotationIsWorldLocked(avatar, *item, newLockState); + } + + refreshTextHighlightingOnJointScrollLists(); +} diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 78d46f0b90..6ff2d2b98c 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -268,6 +268,7 @@ public: void onCommitSpinner(const LLUICtrl* spinner, const S32 ID); void onCommitSlider(const LLUICtrl* slider, const S32 id); void onClickSymmetrize(const S32 ID); + void onClickLockWorldRotBtn(); // UI Refreshments void refreshRotationSlidersAndSpinners(); @@ -490,6 +491,7 @@ public: LLButton* mFlipJointBtn{ nullptr }; LLButton* mRecaptureBtn{ nullptr }; LLButton* mTogglePosingBonesBtn{ nullptr }; + LLButton* mToggleLockWorldRotBtn{ nullptr }; LLButton* mToggleMirrorRotationBtn{ nullptr }; LLButton* mToggleSympatheticRotationBtn{ nullptr }; LLButton* mToggleDeltaModeBtn{ nullptr }; diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index c0c6b365b5..2e4bab90ba 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -155,14 +155,14 @@ void FSJointPose::recaptureJoint() mCurrentState = FSJointState(joint); } -void FSJointPose::recaptureJointAsDelta(bool zeroBase) +LLQuaternion FSJointPose::recaptureJointAsDelta(bool zeroBase) { LLJoint* joint = mJointState->getJoint(); if (!joint) - return; + return LLQuaternion::DEFAULT; addStateToUndo(FSJointState(mCurrentState)); - mCurrentState.updateFromJoint(joint, zeroBase); + return mCurrentState.updateFromJoint(joint, zeroBase); } void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) @@ -241,6 +241,22 @@ bool FSJointPose::userHaseSetBaseRotationToZero() const return mCurrentState.userSetBaseRotationToZero(); } +bool FSJointPose::getWorldRotationLockState() const +{ + if (mIsCollisionVolume) + return false; + + return mCurrentState.mRotationIsWorldLocked; +} + +void FSJointPose::setWorldRotationLockState(bool newState) +{ + if (mIsCollisionVolume) + return; + + mCurrentState.mRotationIsWorldLocked = newState; +} + bool FSJointPose::canPerformUndo() const { switch (mLastSetJointStates.size()) diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index 4f4eb81205..1428b930b5 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -161,7 +161,8 @@ class FSJointPose /// Recalculates the delta reltive to the base for a new rotation. /// /// Whether to zero the base rotation on setting the supplied rotation. - void recaptureJointAsDelta(bool zeroBase); + /// The rotation of the public difference between before and after recapture. + LLQuaternion recaptureJointAsDelta(bool zeroBase); /// /// Clears the undo/redo deque. @@ -174,6 +175,18 @@ class FSJointPose /// True if the user performed some action to specify zero rotation as the base, otherwise false. bool userHaseSetBaseRotationToZero() const; + /// + /// Gets whether the rotation of a joint has been 'locked' so that its world rotation can remain constant while parent joints change. + /// + /// True if the joint is rotationally locked to the world, otherwise false. + bool getWorldRotationLockState() const; + + /// + /// Sets whether the world-rotation of a joint has been 'locked' so that as its parent joints change rotation or position, this joint keeps a constant world rotation. + /// + /// The new state for the world-rotation lock. + void setWorldRotationLockState(bool newState); + /// /// Reverts the position/rotation/scale to their values when the animation begun. /// This treatment is required for certain joints, particularly Collision Volumes and those bones not commonly animated by an AO. @@ -201,16 +214,9 @@ class FSJointPose } FSJointState() = default; - LLQuaternion mDeltaRotation; LLQuaternion getTargetRotation() const { return mRotation * mBaseRotation; } LLVector3 getTargetPosition() const { return mPosition + mBasePosition; } LLVector3 getTargetScale() const { return mScale + mBaseScale; } - void updateRotation(const LLQuaternion& newRotation) - { - auto inv_base = mBaseRotation; - inv_base.conjugate(); - mDeltaRotation = newRotation * inv_base; - }; void reflectRotation() { @@ -234,6 +240,7 @@ class FSJointPose void resetJoint() { mUserSpecifiedBaseZero = false; + mRotationIsWorldLocked = false; mBaseRotation.set(mStartingRotation); mRotation.set(LLQuaternion::DEFAULT); mPosition.setZero(); @@ -256,20 +263,24 @@ class FSJointPose joint->setScale(mBaseScale); } - void updateFromJoint(LLJoint* joint, bool zeroBase) + LLQuaternion updateFromJoint(LLJoint* joint, bool zeroBase) { if (!joint) - return; + return LLQuaternion::DEFAULT; + LLQuaternion initalPublicRot = mRotation; LLQuaternion invRot = mBaseRotation; invRot.conjugate(); - mRotation = joint->getRotation() * invRot; + LLQuaternion newPublicRot = joint->getRotation() * invRot; if (zeroBase) zeroBaseRotation(); + mRotation.set(newPublicRot); mPosition.set(joint->getPosition() - mBasePosition); mScale.set(joint->getScale() - mBaseScale); + + return newPublicRot *= ~initalPublicRot; } private: @@ -284,12 +295,14 @@ class FSJointPose mPosition.set(state->mPosition); mScale.set(state->mScale); mUserSpecifiedBaseZero = state->userSetBaseRotationToZero(); + mRotationIsWorldLocked = state->mRotationIsWorldLocked; } public: LLQuaternion mRotation; LLVector3 mPosition; LLVector3 mScale; + bool mRotationIsWorldLocked = false; private: LLQuaternion mStartingRotation; diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index 81570866f8..c8b968f393 100644 --- a/indra/newview/fsposeranimator.cpp +++ b/indra/newview/fsposeranimator.cpp @@ -95,6 +95,7 @@ void FSPoserAnimator::undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint return; jointPose->undoLastChange(); + undoOrRedoWorldLockedDescendants(joint, posingMotion, false); if (style == NONE || style == DELTAMODE) return; @@ -104,6 +105,10 @@ void FSPoserAnimator::undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint return; oppositeJointPose->undoLastChange(); + + auto oppositePoserJoint = getPoserJointByName(joint.mirrorJointName()); + if (oppositePoserJoint) + undoOrRedoWorldLockedDescendants(*oppositePoserJoint, posingMotion, false); } void FSPoserAnimator::resetJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) @@ -173,6 +178,7 @@ void FSPoserAnimator::redoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint return; jointPose->redoLastChange(); + undoOrRedoWorldLockedDescendants(joint, posingMotion, true); if (style == NONE || style == DELTAMODE) return; @@ -182,6 +188,10 @@ void FSPoserAnimator::redoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint return; oppositeJointPose->redoLastChange(); + + auto oppositePoserJoint = getPoserJointByName(joint.mirrorJointName()); + if (oppositePoserJoint) + undoOrRedoWorldLockedDescendants(*oppositePoserJoint, posingMotion, true); } LLVector3 FSPoserAnimator::getJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint) const @@ -274,9 +284,23 @@ bool FSPoserAnimator::getRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoser if (!jointPose) return false; - // TODO: FIRE-35769 + return jointPose->getWorldRotationLockState(); +} - return false; +void FSPoserAnimator::setRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint, bool newState) +{ + if (!isAvatarSafeToUse(avatar)) + return; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return; + + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); + if (!jointPose) + return; + + jointPose->setWorldRotationLockState(newState); } bool FSPoserAnimator::exportRotationWillLockJoint(LLVOAvatar* avatar, const FSPoserJoint& joint) const @@ -382,7 +406,9 @@ void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoi if (!jointPose) return; - jointPose->recaptureJointAsDelta(resetBaseRotationToZero); + LLQuaternion deltaRot = jointPose->recaptureJointAsDelta(resetBaseRotationToZero); + + deRotateWorldLockedDescendants(joint, posingMotion, deltaRot); if (style == NONE || style == DELTAMODE) return; @@ -491,6 +517,7 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j case DELTAMODE: jointPose->setPublicRotation(resetBaseRotationToZero, deltaRot * jointPose->getPublicRotation()); + deRotateWorldLockedDescendants(joint, posingMotion, deltaRot); return; case NONE: @@ -500,31 +527,42 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j else jointPose->setPublicRotation(resetBaseRotationToZero, absRot); + deRotateWorldLockedDescendants(joint, posingMotion, deltaRot); return; } + deRotateWorldLockedDescendants(joint, posingMotion, deltaRot); + + auto oppositePoserJoint = getPoserJointByName(joint->mirrorJointName()); FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint->mirrorJointName()); if (!oppositeJointPose) return; - LLQuaternion inv_quat; + LLQuaternion inv_quat = LLQuaternion(-deltaRot.mQ[VX], deltaRot.mQ[VY], -deltaRot.mQ[VZ], deltaRot.mQ[VW]); switch (deflectionStyle) { case SYMPATHETIC: oppositeJointPose->cloneRotationFrom(jointPose); + if (oppositePoserJoint) + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, deltaRot); break; case SYMPATHETIC_DELTA: oppositeJointPose->setPublicRotation(resetBaseRotationToZero, deltaRot * oppositeJointPose->getPublicRotation()); + if (oppositePoserJoint) + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, deltaRot); break; case MIRROR: oppositeJointPose->mirrorRotationFrom(jointPose); + if (oppositePoserJoint) + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, inv_quat); break; case MIRROR_DELTA: - inv_quat = LLQuaternion(-deltaRot.mQ[VX], deltaRot.mQ[VY], -deltaRot.mQ[VZ], deltaRot.mQ[VW]); oppositeJointPose->setPublicRotation(resetBaseRotationToZero, inv_quat * oppositeJointPose->getPublicRotation()); + if (oppositePoserJoint) + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, inv_quat); break; default: @@ -987,3 +1025,98 @@ int FSPoserAnimator::getChildJointDepth(const FSPoserJoint* joint, int depth) co return depth; } + +void FSPoserAnimator::deRotateWorldLockedDescendants(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion rotationChange) +{ + size_t numberOfBvhChildNodes = joint->bvhChildren().size(); + if (numberOfBvhChildNodes < 1) + return; + + FSJointPose* parentJoint = posingMotion->getJointPoseByJointName(joint->jointName()); + if (!parentJoint) + return; + + LLJoint* pJoint = parentJoint->getJointState()->getJoint(); + + for (size_t index = 0; index != numberOfBvhChildNodes; ++index) + { + auto nextJoint = getPoserJointByName(joint->bvhChildren()[index]); + if (!nextJoint) + continue; + + deRotateJointOrFirstLockedChild(nextJoint, posingMotion, pJoint->getWorldRotation(), rotationChange); + } +} + +void FSPoserAnimator::deRotateJointOrFirstLockedChild(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion rotatedParentWorldRot, LLQuaternion rotationChange) +{ + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint->jointName()); + if (!jointPose) + return; + + if (jointPose->getWorldRotationLockState()) + { + LLQuaternion worldRotOfThisJoint = jointPose->getJointState()->getJoint()->getWorldRotation(); + LLQuaternion differenceInWorldRot = worldRotOfThisJoint * ~rotatedParentWorldRot; + LLQuaternion rotDiffInChildFrame = differenceInWorldRot * rotationChange * ~differenceInWorldRot; + rotDiffInChildFrame.conjugate(); + + jointPose->setPublicRotation(false, rotDiffInChildFrame * jointPose->getPublicRotation()); + return; + } + + size_t numberOfBvhChildNodes = joint->bvhChildren().size(); + if (numberOfBvhChildNodes < 1) + return; + + for (size_t index = 0; index != numberOfBvhChildNodes; ++index) + { + auto nextJoint = getPoserJointByName(joint->bvhChildren()[index]); + if (!nextJoint) + continue; + + deRotateJointOrFirstLockedChild(nextJoint, posingMotion, rotatedParentWorldRot, rotationChange); + } +} + +void FSPoserAnimator::undoOrRedoWorldLockedDescendants(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo) +{ + size_t numberOfBvhChildNodes = joint.bvhChildren().size(); + if (numberOfBvhChildNodes < 1) + return; + + for (size_t index = 0; index != numberOfBvhChildNodes; ++index) + { + auto nextJoint = getPoserJointByName(joint.bvhChildren()[index]); + if (!nextJoint) + continue; + + undoOrRedoJointOrFirstLockedChild(*nextJoint, posingMotion, redo); + } +} + +void FSPoserAnimator::undoOrRedoJointOrFirstLockedChild(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo) +{ + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); + if (!jointPose) + return; + + if (jointPose->getWorldRotationLockState()) + { + redo ? jointPose->redoLastChange() : jointPose->undoLastChange(); + return; + } + + size_t numberOfBvhChildNodes = joint.bvhChildren().size(); + if (numberOfBvhChildNodes < 1) + return; + + for (size_t index = 0; index != numberOfBvhChildNodes; ++index) + { + auto nextJoint = getPoserJointByName(joint.bvhChildren()[index]); + if (!nextJoint) + continue; + + undoOrRedoJointOrFirstLockedChild(*nextJoint, posingMotion, redo); + } +} diff --git a/indra/newview/fsposeranimator.h b/indra/newview/fsposeranimator.h index c7995cf5ea..3e81d0c2f4 100644 --- a/indra/newview/fsposeranimator.h +++ b/indra/newview/fsposeranimator.h @@ -608,6 +608,14 @@ public: /// True if the joint is maintaining a fixed-rotation in world, otherwise false. bool getRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint) const; + /// + /// Sets the world-rotation-lock status for supplied joint for the supplied avatar. + /// + /// The avatar owning the supplied joint. + /// The joint to query. + /// The lock state to apply. + void setRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint, bool newState); + /// /// Determines if the kind of save to perform should be a 'delta' save, or a complete save. /// @@ -732,6 +740,45 @@ public: /// The number of generations of descendents the joint has, if none, then zero. int getChildJointDepth(const FSPoserJoint* joint, int depth) const; + /// + /// Derotates the first world-locked child joint to the supplied joint. + /// + /// The edited joint, whose children may be world-locked. + /// The posing motion. + /// The rotation the supplied joint was/is being changed by. + /// + /// There are two ways to resolve this problem: before the rotation is applied in the PosingMotion (the animation) or after. + /// If performed after, a feedback loop is created, because you're noting the world-rotation in one frame, then correcting it back to that in another. + /// This implementation works by applying an opposing-rotation to the locked child joint which is corrected for the relative world-rotations of parent and child. + /// + void deRotateWorldLockedDescendants(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion rotation); + + /// + /// Recursively tests the supplied joint and all its children for their world-locked status, and applies a de-rotation if it is world-locked. + /// + /// The edited joint, whose children may be world-locked. + /// The posing motion. + /// The world-rotation of the joint that was edited. + /// The rotation the joint was edit is being changed by. + void deRotateJointOrFirstLockedChild(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion parentWorldRot, + LLQuaternion rotation); + + /// + /// Performs an undo or redo of an edit to the supplied joints world-locked descendants. + /// + /// The edited joint, whose children may be world-locked. + /// The posing motion. + /// Whether to redo the edit, otherwise the edit is undone. + void undoOrRedoWorldLockedDescendants(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo); + + /// + /// Recursively tests the supplied joint and all its children for their world-locked status, and applies an undo or redo if it is world-locked. + /// + /// The joint which will have the undo or redo performed, if it is world locked. + /// The posing motion. + /// Whether to redo the edit, otherwise the edit is undone. + void undoOrRedoJointOrFirstLockedChild(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo); + /// /// Maps the avatar's ID to the animation registered to them. /// Thus we start/stop the same animation, and get/set the same rotations etc. diff --git a/indra/newview/skins/default/xui/en/floater_fs_poser.xml b/indra/newview/skins/default/xui/en/floater_fs_poser.xml index 96c3923afc..c170ac8265 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -1751,6 +1751,20 @@ width="430"> + left_pad="0" top_delta="0" name="button_spacer_panel" - width="80"/> + width="59"/>