From 134b44c88a715ac86bfd03e693470ef81b2c9790 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sun, 16 Mar 2025 02:20:54 +0000 Subject: [PATCH 01/13] Poser fixes and refactors Refactor UI to cache rot/pos/scale in spinners instead of sliders Remove a duplicate UI callback Enable/Disable Undo button based on queue Update how changes from Manip are determined and stored Changes from Manip will now save to XML/BVH Allow Manip to mirror/copy rotations (pos/scale todo) Manip changes to Pos and Scale are also stored, and should still revert Collision volume changes from Manip now 'stick' Undo queue now purge when base-rotation is set to zero (when you want to BVH) --- indra/newview/fsfloaterposer.cpp | 290 ++++++++++-------- indra/newview/fsfloaterposer.h | 38 +-- indra/newview/fsjointpose.cpp | 36 ++- indra/newview/fsjointpose.h | 24 +- indra/newview/fsmaniprotatejoint.cpp | 4 +- indra/newview/fsposeranimator.cpp | 36 ++- indra/newview/fsposeranimator.h | 14 +- indra/newview/fsposingmotion.cpp | 2 +- indra/newview/fsposingmotion.h | 4 +- .../skins/default/xui/en/floater_fs_poser.xml | 27 +- 10 files changed, 287 insertions(+), 188 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 9a54e1d7bf..51ef0be8b6 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -83,11 +83,10 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) mCommitCallbackRegistrar.add("Poser.ToggleSympatheticChanges", [this](LLUICtrl*, const LLSD&) { onToggleSympatheticChange(); }); mCommitCallbackRegistrar.add("Poser.AdjustTrackPadSensitivity", [this](LLUICtrl*, const LLSD&) { onAdjustTrackpadSensitivity(); }); - mCommitCallbackRegistrar.add("Poser.PositionSet", [this](LLUICtrl*, const LLSD&) { onAvatarPositionSet(); }); + mCommitCallbackRegistrar.add("Poser.PositionSet", [this](LLUICtrl*, const LLSD&) { onPositionSet(); }); mCommitCallbackRegistrar.add("Poser.SetToTPose", [this](LLUICtrl*, const LLSD&) { onSetAvatarToTpose(); }); - mCommitCallbackRegistrar.add("Poser.Advanced.PositionSet", [this](LLUICtrl*, const LLSD&) { onAdvancedPositionSet(); }); - mCommitCallbackRegistrar.add("Poser.Advanced.ScaleSet", [this](LLUICtrl*, const LLSD&) { onAdvancedScaleSet(); }); + mCommitCallbackRegistrar.add("Poser.Advanced.ScaleSet", [this](LLUICtrl*, const LLSD&) { onScaleSet(); }); mCommitCallbackRegistrar.add("Poser.UndoLastPosition", [this](LLUICtrl*, const LLSD&) { onUndoLastChange(); }); mCommitCallbackRegistrar.add("Poser.RedoLastPosition", [this](LLUICtrl*, const LLSD&) { onRedoLastChange(); }); mCommitCallbackRegistrar.add("Poser.ResetJoint", [this](LLUICtrl*, const LLSD& data) { onResetJoint(data); }); @@ -104,13 +103,14 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) mCommitCallbackRegistrar.add("Poser.TogglePosingSelectedBones", [this](LLUICtrl*, const LLSD&) { onClickToggleSelectedBoneEnabled(); }); mCommitCallbackRegistrar.add("Poser.CommitSpinner", [this](LLUICtrl* spinner, const LLSD& data) { onCommitSpinner(spinner, data); }); + mCommitCallbackRegistrar.add("Poser.CommitSlider", [this](LLUICtrl* slider, const LLSD& data) { onCommitSlider(slider, data); }); mCommitCallbackRegistrar.add("Poser.Symmetrize", [this](LLUICtrl*, const LLSD& data) { onClickSymmetrize(data); }); } bool FSFloaterPoser::postBuild() { mAvatarTrackball = getChild("limb_rotation"); - mAvatarTrackball->setCommitCallback([this](LLUICtrl *, const LLSD &) { onLimbTrackballChanged(); }); + mAvatarTrackball->setCommitCallback([this](LLUICtrl *, const LLSD &) { onTrackballChanged(); }); mJointsTabs = getChild("joints_tabs"); mJointsTabs->setCommitCallback( @@ -188,6 +188,7 @@ bool FSFloaterPoser::postBuild() mToggleSympatheticRotationBtn = getChild("button_toggleSympatheticRotation"); mToggleDeltaModeBtn = getChild("delta_mode_toggle"); mRedoChangeBtn = getChild("button_redo_change"); + mUndoChangeBtn = getChild("undo_change"); mSetToTposeButton = getChild("set_t_pose_button"); mJointsParentPnl = getChild("joints_parent_panel"); @@ -552,6 +553,7 @@ void FSFloaterPoser::onClickFlipSelectedJoints() } refreshRotationSlidersAndSpinners(); + enableOrDisableRedoAndUndoButton(); refreshTrackpadCursor(); } @@ -596,13 +598,11 @@ void FSFloaterPoser::onClickRecaptureSelectedBones() refreshRotationSlidersAndSpinners(); refreshTrackpadCursor(); refreshTextHighlightingOnJointScrollLists(); + enableOrDisableRedoAndUndoButton(); } -void FSFloaterPoser::updatePosedBones() -{ - auto selectedJoints = getUiSelectedPoserJoints(); - if (selectedJoints.size() < 1) - return; +void FSFloaterPoser::updatePosedBones(const std::string& jointName) +{ LLVOAvatar *avatar = getUiSelectedAvatar(); if (!avatar) return; @@ -610,18 +610,17 @@ void FSFloaterPoser::updatePosedBones() if (!mPoserAnimator.isPosingAvatar(avatar)) return; - for (auto item : selectedJoints) - { - bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (!currentlyPosing) - continue; + const FSPoserAnimator::FSPoserJoint* poserJoint = mPoserAnimator.getPoserJointByName(jointName); + if (!poserJoint) + return; - mPoserAnimator.recaptureJointAsDelta(avatar, *item, getJointTranslation(item->jointName()), getJointNegation(item->jointName())); - } + mPoserAnimator.recaptureJointAsDelta(avatar, poserJoint, getUiSelectedBoneDeflectionStyle()); - setSavePosesButtonText(true); refreshRotationSlidersAndSpinners(); + refreshPositionSlidersAndSpinners(); + refreshScaleSlidersAndSpinners(); refreshTrackpadCursor(); + enableOrDisableRedoAndUndoButton(); refreshTextHighlightingOnJointScrollLists(); } @@ -633,7 +632,7 @@ void FSFloaterPoser::onClickBrowsePoseCache() gViewerWindow->getWindow()->openFile(pathname); } -void FSFloaterPoser::onClickSymmetrize(S32 ID) +void FSFloaterPoser::onClickSymmetrize(const S32 ID) { if (notDoubleClicked()) return; @@ -648,40 +647,24 @@ void FSFloaterPoser::onClickSymmetrize(S32 ID) mPoserAnimator.symmetrizeLeftToRightOrRightToLeft(avatar, ID == 2); refreshRotationSlidersAndSpinners(); + enableOrDisableRedoAndUndoButton(); refreshTrackpadCursor(); } -void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner, S32 id) +void FSFloaterPoser::onCommitSpinner(const LLUICtrl* spinner, const S32 id) { if (!spinner) return; - auto activeTab = mJointsTabs->getCurrentPanel(); - if (!activeTab) - return; - - bool changingBodyPosition = activeTab == mPositionRotationPnl; - F32 value = (F32)spinner->getValue().asReal(); switch (id) { case 0: // av_position_updown_spinner - { - mPosZSlider->setValue(value); - onAvatarPositionSet(); - break; - } case 1: // av_position_leftright - { - mPosYSlider->setValue(value); - onAvatarPositionSet(); - break; - } case 2: // av_position_inout_spinner { - mPosXSlider->setValue(value); - onAvatarPositionSet(); + onPositionSet(); break; } case 3: // trackpad_sensitivity_spinner @@ -691,49 +674,91 @@ void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner, S32 id) } case 7: // adv_posx_spinner { - if (changingBodyPosition) - mPosXSlider->setValue(value); - - mAdvPosXSlider->setValue(value); - onAdvancedPositionSet(); + mInOutSpnr->setValue(value); + onPositionSet(); break; } case 8: // adv_posy_spinner { - if (changingBodyPosition) - mPosYSlider->setValue(value); - - mAdvPosYSlider->setValue(value); - onAdvancedPositionSet(); + mLeftRightSpnr->setValue(value); + onPositionSet(); break; } case 9: // adv_posz_spinner { - if (changingBodyPosition) - mPosZSlider->setValue(value); - - mAdvPosZSlider->setValue(value); - onAdvancedPositionSet(); + mUpDownSpnr->setValue(value); + onPositionSet(); break; } case 10: // adv_scalex_spinner - { - mAdvScaleXSlider->setValue(value); - onAdvancedScaleSet(); - break; - } case 11: // adv_scaley_spinner - { - mAdvScaleYSlider->setValue(value); - onAdvancedScaleSet(); - break; - } case 12: // adv_scalez_spinner { - mAdvScaleZSlider->setValue(value); - onAdvancedScaleSet(); + onScaleSet(); break; } + + default: + LL_WARNS("Posing") << "onCommitSpinner passed invalid parameter: " << id << LL_ENDL; + break; + } +} + +void FSFloaterPoser::onCommitSlider(const LLUICtrl* slider, const S32 id) +{ + if (!slider) + return; + + F32 value = (F32)slider->getValue().asReal(); + + switch (id) + { + case 0: // av_position_updown + case 9: // Advanced_Position_Z + { + mUpDownSpnr->setValue(value); + onPositionSet(); + break; + } + + case 1: // av_position_leftright + case 8: // Advanced_Position_Y + { + mLeftRightSpnr->setValue(value); + onPositionSet(); + break; + } + + case 2: // av_position_inout + case 7: // Advanced_Position_X + { + mInOutSpnr->setValue(value); + onPositionSet(); + break; + } + + case 10: // Advanced_Scale_X + { + mScaleXSpnr->setValue(value); + onScaleSet(); + break; + } + case 11: // Advanced_Scale_Y + { + mScaleYSpnr->setValue(value); + onScaleSet(); + break; + } + case 12: // Advanced_Scale_Z + { + mScaleZSpnr->setValue(value); + onScaleSet(); + break; + } + + default: + LL_WARNS("Posing") << "onCommitSlider passed invalid parameter: " << id << LL_ENDL; + break; } } @@ -1295,12 +1320,11 @@ void FSFloaterPoser::onUndoLastChange() mPoserAnimator.undoLastJointChange(avatar, *item, getUiSelectedBoneDeflectionStyle()); } - enableOrDisableRedoButton(); + enableOrDisableRedoAndUndoButton(); refreshRotationSlidersAndSpinners(); - refreshTrackpadCursor(); refreshPositionSlidersAndSpinners(); - refreshAvatarPositionSlidersAndSpinners(); refreshScaleSlidersAndSpinners(); + refreshTrackpadCursor(); } void FSFloaterPoser::onSetAvatarToTpose() @@ -1315,6 +1339,7 @@ void FSFloaterPoser::onSetAvatarToTpose() setSavePosesButtonText(false); mPoserAnimator.setAllAvatarStartingRotationsToZero(avatar); refreshTextHighlightingOnJointScrollLists(); + enableOrDisableRedoAndUndoButton(); } void FSFloaterPoser::onResetJoint(const LLSD data) @@ -1345,10 +1370,10 @@ void FSFloaterPoser::onResetJoint(const LLSD data) } refreshRotationSlidersAndSpinners(); - refreshTrackpadCursor(); - refreshAvatarPositionSlidersAndSpinners(); refreshPositionSlidersAndSpinners(); refreshScaleSlidersAndSpinners(); + refreshTrackpadCursor(); + enableOrDisableRedoAndUndoButton(); } void FSFloaterPoser::onRedoLastChange() @@ -1371,16 +1396,18 @@ void FSFloaterPoser::onRedoLastChange() mPoserAnimator.redoLastJointChange(avatar, *item, getUiSelectedBoneDeflectionStyle()); } - enableOrDisableRedoButton(); + enableOrDisableRedoAndUndoButton(); refreshRotationSlidersAndSpinners(); refreshTrackpadCursor(); refreshScaleSlidersAndSpinners(); refreshPositionSlidersAndSpinners(); - refreshAvatarPositionSlidersAndSpinners(); } -void FSFloaterPoser::enableOrDisableRedoButton() +void FSFloaterPoser::enableOrDisableRedoAndUndoButton() { + mRedoChangeBtn->setEnabled(false); + mUndoChangeBtn->setEnabled(false); + LLVOAvatar* avatar = getUiSelectedAvatar(); if (!avatar) return; @@ -1393,14 +1420,20 @@ void FSFloaterPoser::enableOrDisableRedoButton() return; bool shouldEnableRedoButton = false; + bool shouldEnableUndoButton = false; + for (auto item : selectedJoints) { bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - shouldEnableRedoButton |= mPoserAnimator.canRedoJointChange(avatar, *item); + if (!currentlyPosing) + continue; + + shouldEnableRedoButton |= mPoserAnimator.canRedoOrUndoJointChange(avatar, *item); + shouldEnableUndoButton |= mPoserAnimator.canRedoOrUndoJointChange(avatar, *item, true); } mRedoChangeBtn->setEnabled(shouldEnableRedoButton); + mUndoChangeBtn->setEnabled(shouldEnableUndoButton); } void FSFloaterPoser::onToggleVisualManipulators() @@ -1663,54 +1696,43 @@ LLVOAvatar* FSFloaterPoser::getAvatarByUuid(const LLUUID& avatarToFind) const return nullptr; } -void FSFloaterPoser::onAdvancedPositionSet() +void FSFloaterPoser::onPositionSet() { - F32 posX = mAdvPosXSlider->getValueF32(); - F32 posY = mAdvPosYSlider->getValueF32(); - F32 posZ = mAdvPosZSlider->getValueF32(); + F32 posX = (F32)mInOutSpnr->getValue().asReal(); + F32 posY = (F32)mLeftRightSpnr->getValue().asReal(); + F32 posZ = (F32)mUpDownSpnr->getValue().asReal(); mAdvPosXSpnr->setValue(posX); - mInOutSpnr->setValue(posX); mAdvPosYSpnr->setValue(posY); - mLeftRightSpnr->setValue(posY); mAdvPosZSpnr->setValue(posZ); - mUpDownSpnr->setValue(posZ); - - setSelectedJointsPosition(posX, posY, posZ); - refreshAvatarPositionSlidersAndSpinners(); -} - -void FSFloaterPoser::onAdvancedScaleSet() -{ - F32 scX = mAdvScaleXSlider->getValueF32(); - F32 scY = mAdvScaleYSlider->getValueF32(); - F32 scZ = mAdvScaleZSlider->getValueF32(); - - mScaleXSpnr->setValue(scX); - mScaleYSpnr->setValue(scY); - mScaleZSpnr->setValue(scZ); - - setSelectedJointsScale(scX, scY, scZ); -} - -void FSFloaterPoser::onAvatarPositionSet() -{ - F32 posX = mPosXSlider->getValueF32(); - F32 posY = mPosYSlider->getValueF32(); - F32 posZ = mPosZSlider->getValueF32(); - - mAdvPosXSpnr->setValue(posX); - mInOutSpnr->setValue(posX); - mAdvPosYSpnr->setValue(posY); - mLeftRightSpnr->setValue(posY); - mAdvPosZSpnr->setValue(posZ); - mUpDownSpnr->setValue(posZ); + mAdvPosXSlider->setValue(posX); + mAdvPosYSlider->setValue(posY); + mAdvPosZSlider->setValue(posZ); + mPosXSlider->setValue(posX); + mPosYSlider->setValue(posY); + mPosZSlider->setValue(posZ); setSelectedJointsPosition(posX, posY, posZ); refreshPositionSlidersAndSpinners(); + enableOrDisableRedoAndUndoButton(); } -void FSFloaterPoser::onLimbTrackballChanged() +void FSFloaterPoser::onScaleSet() +{ + F32 scX = (F32)mScaleXSpnr->getValue().asReal(); + F32 scY = (F32)mScaleYSpnr->getValue().asReal(); + F32 scZ = (F32)mScaleZSpnr->getValue().asReal(); + + mAdvScaleXSlider->setValue(scX); + mAdvScaleYSlider->setValue(scY); + mAdvScaleZSlider->setValue(scZ); + + setSelectedJointsScale(scX, scY, scZ); + refreshScaleSlidersAndSpinners(); + enableOrDisableRedoAndUndoButton(); +} + +void FSFloaterPoser::onTrackballChanged() { LLVector3 trackPadPos, trackPadDeltaPos; LLSD position = mAvatarTrackball->getValue(); @@ -1746,6 +1768,8 @@ void FSFloaterPoser::onLimbTrackballChanged() mYawSpnr->setValue(trackPadPos.mV[VX] *= RAD_TO_DEG); mPitchSpnr->setValue(trackPadPos.mV[VY] *= RAD_TO_DEG); mRollSpnr->setValue(trackPadPos.mV[VZ] *= RAD_TO_DEG); + + enableOrDisableRedoAndUndoButton(); } F32 FSFloaterPoser::unWrapScale(F32 scale) @@ -1787,6 +1811,8 @@ void FSFloaterPoser::onYawPitchRollChanged() absoluteRotation.mV[VZ] /= NormalTrackpadRangeInRads; mAvatarTrackball->setValue(absoluteRotation.getValue()); + + enableOrDisableRedoAndUndoButton(); } void FSFloaterPoser::onAdjustTrackpadSensitivity() @@ -1807,27 +1833,6 @@ void FSFloaterPoser::refreshTrackpadCursor() mAvatarTrackball->setValue(axis1, axis2, axis3); } -/// -/// This only sets the position sliders of the 'basic' view (not the advanced sliders). -/// -void FSFloaterPoser::refreshAvatarPositionSlidersAndSpinners() -{ - auto activeTab = mJointsTabs->getCurrentPanel(); - if (!activeTab) - return; - - if (activeTab != mPositionRotationPnl) - return; // if the active tab isn't the av position one, don't set anything. - - LLVector3 position = getPositionOfFirstSelectedJoint(); - mPosXSlider->setValue(position.mV[VX]); - mInOutSpnr->setValue(position.mV[VX]); - mPosYSlider->setValue(position.mV[VY]); - mLeftRightSpnr->setValue(position.mV[VY]); - mPosZSlider->setValue(position.mV[VZ]); - mUpDownSpnr->setValue(position.mV[VZ]); -} - void FSFloaterPoser::refreshRotationSlidersAndSpinners() { LLVector3 rotation = getRotationOfFirstSelectedJoint(); @@ -1848,6 +1853,20 @@ void FSFloaterPoser::refreshPositionSlidersAndSpinners() mAdvPosYSpnr->setValue(position.mV[VY]); mAdvPosZSlider->setValue(position.mV[VZ]); mAdvPosZSpnr->setValue(position.mV[VZ]); + + auto activeTab = mJointsTabs->getCurrentPanel(); + if (!activeTab) + return; + + if (activeTab != mPositionRotationPnl) + return; // if the active tab isn't the av position one, don't set anything. + + mPosXSlider->setValue(position.mV[VX]); + mInOutSpnr->setValue(position.mV[VX]); + mPosYSlider->setValue(position.mV[VY]); + mLeftRightSpnr->setValue(position.mV[VY]); + mPosZSlider->setValue(position.mV[VZ]); + mUpDownSpnr->setValue(position.mV[VZ]); } void FSFloaterPoser::refreshScaleSlidersAndSpinners() @@ -2003,11 +2022,10 @@ LLVector3 FSFloaterPoser::getScaleOfFirstSelectedJoint() const void FSFloaterPoser::onJointTabSelect() { - refreshAvatarPositionSlidersAndSpinners(); + refreshPositionSlidersAndSpinners(); refreshRotationSlidersAndSpinners(); refreshTrackpadCursor(); - enableOrDisableRedoButton(); - refreshPositionSlidersAndSpinners(); + enableOrDisableRedoAndUndoButton(); refreshScaleSlidersAndSpinners(); onClickSetBaseRotZero(); } diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 3d21953b05..9751250e65 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -78,13 +78,13 @@ class FSFloaterPoser : public LLFloater, public LLEditMenuHandler { friend class LLFloaterReg; FSFloaterPoser(const LLSD &key); -public: - void updatePosedBones(); +public: + void updatePosedBones(const std::string& jointName); void selectJointByName(const std::string& jointName); - void undo() override { onUndoLastChange(); }; - bool canUndo() const override { return true; } - void redo() override { onRedoLastChange(); }; - bool canRedo() const override { return true; } + void undo() override { onUndoLastChange(); }; + bool canUndo() const override { return true; } + void redo() override { onRedoLastChange(); }; + bool canRedo() const override { return true; } private: bool postBuild() override; void onOpen(const LLSD& key) override; @@ -223,31 +223,29 @@ public: bool poseFileStartsFromTeePose(const std::string& poseFileName); void setPoseSaveFileTextBoxToUiSelectedAvatarSaveFileName(); void setUiSelectedAvatarSaveFileName(const std::string& saveFileName); + void startPosingSelf(); + void stopPosingAllAvatars(); // visual manipulators control void enableVisualManipulators(); void disableVisualManipulators(); - // UI Event Handlers: + // UI Event Handlers void onAvatarsRefresh(); void onAvatarSelect(); void onJointTabSelect(); void onToggleMirrorChange(); void onToggleSympatheticChange(); - void onToggleVisualManipulators(); + void onToggleVisualManipulators(); void setRotationChangeButtons(bool mirror, bool sympathetic); void onUndoLastChange(); void onRedoLastChange(); void onResetJoint(const LLSD data); void onSetAvatarToTpose(); - void enableOrDisableRedoButton(); void onPoseStartStop(); - void startPosingSelf(); - void stopPosingAllAvatars(); - void onLimbTrackballChanged(); + void onTrackballChanged(); void onYawPitchRollChanged(); - void onAvatarPositionSet(); - void onAdvancedPositionSet(); - void onAdvancedScaleSet(); + void onPositionSet(); + void onScaleSet(); void onClickToggleSelectedBoneEnabled(); void onClickRecaptureSelectedBones(); void onClickFlipPose(); @@ -257,15 +255,16 @@ public: void onClickLoadRightHandPose(); void onClickLoadHandPose(bool isRightHand); void onClickSetBaseRotZero(); - void onCommitSpinner(LLUICtrl* spinner, S32 ID); - void onClickSymmetrize(S32 ID); + void onCommitSpinner(const LLUICtrl* spinner, const S32 ID); + void onCommitSlider(const LLUICtrl* slider, const S32 id); + void onClickSymmetrize(const S32 ID); // UI Refreshments void refreshRotationSlidersAndSpinners(); - void refreshAvatarPositionSlidersAndSpinners(); - void refreshTrackpadCursor(); void refreshPositionSlidersAndSpinners(); void refreshScaleSlidersAndSpinners(); + void refreshTrackpadCursor(); + void enableOrDisableRedoAndUndoButton(); /// /// Determines if we have permission to animate the supplied avatar. @@ -492,6 +491,7 @@ public: LLButton* mToggleSympatheticRotationBtn{ nullptr }; LLButton* mToggleDeltaModeBtn{ nullptr }; LLButton* mRedoChangeBtn{ nullptr }; + LLButton* mUndoChangeBtn{ nullptr }; LLButton* mSetToTposeButton{ nullptr }; LLButton* mBtnJointRotate{ nullptr }; diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index 53a6b2869e..608e737aba 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -144,21 +144,15 @@ void FSJointPose::recaptureJoint() addStateToUndo(FSJointState(mCurrentState)); mCurrentState = FSJointState(joint); } + void FSJointPose::recaptureJointAsDelta() { - if (mIsCollisionVolume) - { - return; - } - LLJoint* joint = mJointState->getJoint(); if (!joint) - { return; - } - - addStateToUndo(mCurrentState); - mCurrentState = FSJointState(joint); + + addStateToUndo(FSJointState(mCurrentState)); + mCurrentState.updateFromJoint(joint); } void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) @@ -209,6 +203,9 @@ void FSJointPose::zeroBaseRotation() if (mIsCollisionVolume) return; + if (!isBaseRotationZero()) + purgeUndoQueue(); + mCurrentState.zeroBaseRotation(); } @@ -219,3 +216,22 @@ bool FSJointPose::isBaseRotationZero() const return mCurrentState.baseRotationIsZero(); } + +void FSJointPose::purgeUndoQueue() +{ + mUndoneJointStatesIndex = 0; + mLastSetJointStates.clear(); +} + +bool FSJointPose::canPerformUndo() const +{ + switch (mLastSetJointStates.size()) + { + case 0: // nothing to undo + return false; + case 1: // there is only one change + return true; + default: // current state is not the bottom of the deque + return mUndoneJointStatesIndex != (mLastSetJointStates.size() - 1); + } +} diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index fe2d200bd1..913182e72e 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -108,6 +108,12 @@ class FSJointPose /// True if the represented joint is zero, otherwise false. bool isBaseRotationZero() const; + /// + /// Gets whether an undo of this joint may be performed. + /// + /// true if the joint may have a undo applied, otherwise false. + bool canPerformUndo() const; + /// /// Gets whether a redo of this joint may be performed. /// @@ -143,11 +149,16 @@ class FSJointPose /// Resets the beginning properties of the joint this represents. /// void recaptureJoint(); + /// /// Recalculates the delta reltive to the base for a new rotation. /// void recaptureJointAsDelta(); + /// + /// Clears the undo/redo deque. + /// + void purgeUndoQueue(); /// /// Reverts the position/rotation/scale to their values when the animation begun. @@ -185,7 +196,6 @@ class FSJointPose inv_base.conjugate(); mDeltaRotation = newRotation * inv_base; }; - void reflectRotation() { @@ -215,6 +225,18 @@ class FSJointPose joint->setScale(mBaseScale); } + void updateFromJoint(LLJoint* joint) + { + if (!joint) + return; + + LLQuaternion invRot = mBaseRotation; + invRot.conjugate(); + mRotation = joint->getRotation() * invRot; + mPosition.set(mBasePosition - joint->getPosition()); + mScale.set(mBaseScale - joint->getScale()); + } + private: FSJointState(FSJointState* state) { diff --git a/indra/newview/fsmaniprotatejoint.cpp b/indra/newview/fsmaniprotatejoint.cpp index 8e5c4466da..da1bd33a90 100644 --- a/indra/newview/fsmaniprotatejoint.cpp +++ b/indra/newview/fsmaniprotatejoint.cpp @@ -1012,9 +1012,9 @@ bool FSManipRotateJoint::handleMouseUp(S32 x, S32 y, MASK mask) if (hasMouseCapture()) { // Update the UI, by causing it to read back the position of the selected joints and aply those relative to the base rot - if (poser) + if (poser && mJoint) { - poser->updatePosedBones(); + poser->updatePosedBones(mJoint->getName()); } // Release mouse diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index 39938d4490..62d9030c85 100644 --- a/indra/newview/fsposeranimator.cpp +++ b/indra/newview/fsposeranimator.cpp @@ -138,7 +138,7 @@ void FSPoserAnimator::resetJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, oppositeJointPose->setPublicScale(LLVector3()); } -bool FSPoserAnimator::canRedoJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint) +bool FSPoserAnimator::canRedoOrUndoJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint, bool canUndo) { if (!isAvatarSafeToUse(avatar)) return false; @@ -154,6 +154,9 @@ bool FSPoserAnimator::canRedoJointChange(LLVOAvatar* avatar, const FSPoserJoint& if (!jointPose) return false; + if (canUndo) + return jointPose->canPerformUndo(); + return jointPose->canPerformRedo(); } @@ -303,7 +306,7 @@ void FSPoserAnimator::setAllAvatarStartingRotationsToZero(LLVOAvatar* avatar) if (!posingMotion) return; - posingMotion->setAllRotationsToZero(); + posingMotion->setAllRotationsToZeroAndClearUndo(); } void FSPoserAnimator::recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneAxisTranslation translation, S32 negation) @@ -322,7 +325,8 @@ void FSPoserAnimator::recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joi jointPose->recaptureJoint(); setPosingAvatarJoint(avatar, joint, true); } -void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneAxisTranslation translation, S32 negation) + +void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoint* joint, E_BoneDeflectionStyles style) { if (!isAvatarSafeToUse(avatar)) return; @@ -331,12 +335,34 @@ void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoi if (!posingMotion) return; - FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint->jointName()); if (!jointPose) return; jointPose->recaptureJointAsDelta(); - setPosingAvatarJoint(avatar, joint, true); + + if (style == NONE || style == DELTAMODE) + return; + + FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint->mirrorJointName()); + if (!oppositeJointPose) + return; + + switch (style) + { + case SYMPATHETIC: + case SYMPATHETIC_DELTA: + oppositeJointPose->cloneRotationFrom(jointPose); + break; + + case MIRROR: + case MIRROR_DELTA: + oppositeJointPose->mirrorRotationFrom(jointPose); + break; + + default: + break; + } } LLVector3 FSPoserAnimator::getJointRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneAxisTranslation translation, S32 negation) const diff --git a/indra/newview/fsposeranimator.h b/indra/newview/fsposeranimator.h index 045558288a..2276a9a69f 100644 --- a/indra/newview/fsposeranimator.h +++ b/indra/newview/fsposeranimator.h @@ -417,8 +417,9 @@ public: /// /// The avatar having the joint to which we refer. /// The joint to query. - /// True if a redo action is available, otherwise false. - bool canRedoJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint); + /// Supply true to query if we can perform an Undo, otherwise query redo. + /// True if an undo or redo action is available, otherwise false. + bool canRedoOrUndoJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint, bool canUndo = false); /// /// Re-does the last undone change (rotation, position or scale) to the supplied PoserJoint. @@ -516,7 +517,14 @@ public: /// The axial translation form the supplied joint. /// The style of negation to apply to the recapture. void recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneAxisTranslation translation, S32 negation); - void recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneAxisTranslation translation, S32 negation); + + /// + /// Recaptures any change in joint state. + /// + /// The avatar whose joint is to be recaptured. + /// The joint to recapture. + /// Any ancilliary action to be taken with the change to be made. + void recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoint* joint, E_BoneDeflectionStyles style); /// /// Sets all of the joint rotations of the supplied avatar to zero. diff --git a/indra/newview/fsposingmotion.cpp b/indra/newview/fsposingmotion.cpp index fc418dd911..841ab3a452 100644 --- a/indra/newview/fsposingmotion.cpp +++ b/indra/newview/fsposingmotion.cpp @@ -255,7 +255,7 @@ bool FSPosingMotion::allStartingRotationsAreZero() const return true; } -void FSPosingMotion::setAllRotationsToZero() +void FSPosingMotion::setAllRotationsToZeroAndClearUndo() { for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) { diff --git a/indra/newview/fsposingmotion.h b/indra/newview/fsposingmotion.h index 3b9d3a7a0d..d8d86abf1b 100644 --- a/indra/newview/fsposingmotion.h +++ b/indra/newview/fsposingmotion.h @@ -119,9 +119,9 @@ public: bool allStartingRotationsAreZero() const; /// - /// Sets all of the non-Collision Volume rotations to zero. + /// Sets all of the non-Collision Volume base-and-delta rotations to zero, and clears the undo/redo queue. /// - void setAllRotationsToZero(); + void setAllRotationsToZeroAndClearUndo(); private: /// 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 2a12eb19a9..1cd3cb1ef1 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -369,7 +369,8 @@ width="430"> tool_tip="Move the selected avatar up or down" can_edit_text="true"> + function="Poser.CommitSlider" + parameter="0"/> tool_tip="Move the selected avatar left or right" can_edit_text="true"> + function="Poser.CommitSlider" + parameter="1"/> tool_tip="Move the selected avatar in or out" can_edit_text="true"> + function="Poser.CommitSlider" + parameter="2"/> top_pad="2" width="130" > + function="Poser.CommitSlider" + parameter="7"/> top_pad="2" width="130" > + function="Poser.CommitSlider" + parameter="8"/> top_pad="2" width="130" > + function="Poser.CommitSlider" + parameter="9"/> top_pad="2" width="130" > + function="Poser.CommitSlider" + parameter="10"/> top_pad="2" width="130" > + function="Poser.CommitSlider" + parameter="11"/> top_pad="2" width="130" > + function="Poser.CommitSlider" + parameter="12"/> Date: Sun, 16 Mar 2025 22:35:51 +0000 Subject: [PATCH 02/13] Null check gAgentAvatar usages --- indra/newview/fsfloaterposer.cpp | 37 +++++++++++++++++++++----------- indra/newview/fsfloaterposer.h | 6 ++++++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 51ef0be8b6..425448830f 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -253,15 +253,23 @@ void FSFloaterPoser::onFocusLost() LLEditMenuHandler::gEditMenuHandler = nullptr; } } + void FSFloaterPoser::enableVisualManipulators() { + if (!gAgentAvatarp || gAgentAvatarp.isNull()) + { + mToggleVisualManipulators->setToggleState(false); + return; + } + if (LLToolMgr::getInstance()->getCurrentToolset() != gCameraToolset) { mLastToolset = LLToolMgr::getInstance()->getCurrentToolset(); } + LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); LLToolMgr::getInstance()->getCurrentToolset()->selectTool(FSToolCompPose::getInstance()); - FSToolCompPose::getInstance()->setAvatar( gAgentAvatarp); + FSToolCompPose::getInstance()->setAvatar(gAgentAvatarp); } void FSFloaterPoser::disableVisualManipulators() @@ -1537,6 +1545,7 @@ LLScrollListCtrl* FSFloaterPoser::getScrollListForTab(LLPanel * tabPanel) const LL_WARNS() << "Unknown tab panel: " << tabPanel << LL_ENDL; return nullptr; } + std::vector FSFloaterPoser::getUiSelectedPoserJoints() const { std::vector joints; @@ -1575,21 +1584,23 @@ std::vector FSFloaterPoser::getUiSelectedPoserJo joints.push_back(userData); } } - auto avatarp = getUiSelectedAvatar(); - if (avatarp) - { - if(joints.size() >= 1) - { - FSToolCompPose::getInstance()->setJoint( gAgentAvatarp->getJoint( JointKey::construct(joints[0]->jointName())) ); - } - else - { - FSToolCompPose::getInstance()->setJoint( nullptr ); - } - } + + updateManipWithFirstSelectedJoint(joints); + return joints; } +void FSFloaterPoser::updateManipWithFirstSelectedJoint(std::vector joints) +{ + if (!gAgentAvatarp || gAgentAvatarp.isNull()) + return; + + if (joints.size() >= 1) + FSToolCompPose::getInstance()->setJoint(gAgentAvatarp->getJoint(JointKey::construct(joints[0]->jointName()))); + else + FSToolCompPose::getInstance()->setJoint(nullptr); +} + E_RotationStyle FSFloaterPoser::getUiSelectedBoneRotationStyle(const std::string& jointName) const { if (jointName.empty()) diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 9751250e65..0affb3adb9 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -132,6 +132,12 @@ public: /// The selected joints std::vector getUiSelectedPoserJoints() const; + /// + /// Updates the visual with the first selected joint from the supplied collection, if any. + /// + /// The collection of selected joints. + static void updateManipWithFirstSelectedJoint(std::vector joints); + /// /// Gets a detectable avatar by its UUID. /// From ce786b65e6d92296fe7f0b487a4313cadce548f3 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Wed, 19 Mar 2025 23:35:12 +0000 Subject: [PATCH 03/13] Fix position using incorrect UI values --- indra/newview/fsfloaterposer.cpp | 42 ++++++++++++++++---------------- indra/newview/fsjointpose.h | 4 +-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 425448830f..07e66081e3 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -669,9 +669,20 @@ void FSFloaterPoser::onCommitSpinner(const LLUICtrl* spinner, const S32 id) switch (id) { case 0: // av_position_updown_spinner + { + mAdvPosZSpnr->setValue(value); + onPositionSet(); + break; + } case 1: // av_position_leftright + { + mAdvPosYSpnr->setValue(value); + onPositionSet(); + break; + } case 2: // av_position_inout_spinner { + mAdvPosXSpnr->setValue(value); onPositionSet(); break; } @@ -681,20 +692,9 @@ void FSFloaterPoser::onCommitSpinner(const LLUICtrl* spinner, const S32 id) break; } case 7: // adv_posx_spinner - { - mInOutSpnr->setValue(value); - onPositionSet(); - break; - } case 8: // adv_posy_spinner - { - mLeftRightSpnr->setValue(value); - onPositionSet(); - break; - } case 9: // adv_posz_spinner { - mUpDownSpnr->setValue(value); onPositionSet(); break; } @@ -724,7 +724,7 @@ void FSFloaterPoser::onCommitSlider(const LLUICtrl* slider, const S32 id) case 0: // av_position_updown case 9: // Advanced_Position_Z { - mUpDownSpnr->setValue(value); + mAdvPosZSpnr->setValue(value); onPositionSet(); break; } @@ -732,7 +732,7 @@ void FSFloaterPoser::onCommitSlider(const LLUICtrl* slider, const S32 id) case 1: // av_position_leftright case 8: // Advanced_Position_Y { - mLeftRightSpnr->setValue(value); + mAdvPosYSpnr->setValue(value); onPositionSet(); break; } @@ -740,7 +740,7 @@ void FSFloaterPoser::onCommitSlider(const LLUICtrl* slider, const S32 id) case 2: // av_position_inout case 7: // Advanced_Position_X { - mInOutSpnr->setValue(value); + mAdvPosXSpnr->setValue(value); onPositionSet(); break; } @@ -1709,13 +1709,13 @@ LLVOAvatar* FSFloaterPoser::getAvatarByUuid(const LLUUID& avatarToFind) const void FSFloaterPoser::onPositionSet() { - F32 posX = (F32)mInOutSpnr->getValue().asReal(); - F32 posY = (F32)mLeftRightSpnr->getValue().asReal(); - F32 posZ = (F32)mUpDownSpnr->getValue().asReal(); - - mAdvPosXSpnr->setValue(posX); - mAdvPosYSpnr->setValue(posY); - mAdvPosZSpnr->setValue(posZ); + F32 posX = (F32)mAdvPosXSpnr->getValue().asReal(); + F32 posY = (F32)mAdvPosYSpnr->getValue().asReal(); + F32 posZ = (F32)mAdvPosZSpnr->getValue().asReal(); + + mInOutSpnr->setValue(posX); + mLeftRightSpnr->setValue(posY); + mUpDownSpnr->setValue(posZ); mAdvPosXSlider->setValue(posX); mAdvPosYSlider->setValue(posY); mAdvPosZSlider->setValue(posZ); diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index 913182e72e..05c96475ff 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -233,8 +233,8 @@ class FSJointPose LLQuaternion invRot = mBaseRotation; invRot.conjugate(); mRotation = joint->getRotation() * invRot; - mPosition.set(mBasePosition - joint->getPosition()); - mScale.set(mBaseScale - joint->getScale()); + mPosition.set(joint->getPosition() - mBasePosition); + mScale.set(joint->getScale() - mBaseScale); } private: From e6121931d91b0aed3a5f90c7c61fa0797cf46e43 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sat, 29 Mar 2025 15:21:45 +0000 Subject: [PATCH 04/13] FIRE-34942: Poser: Add option to confirm overwrite when saving a pose/diff --- indra/newview/app_settings/settings.xml | 22 +++---- indra/newview/fsfloaterposer.cpp | 62 +++++++++++++++++-- indra/newview/fsfloaterposer.h | 2 + indra/newview/fsposeranimator.h | 15 ++--- .../skins/default/xui/en/floater_fs_poser.xml | 16 +++++ 5 files changed, 91 insertions(+), 26 deletions(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 3ceca8c98b..655cee4f2c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8056,17 +8056,6 @@ Value 0 - FSPoserAdvancedWindowState - - Comment - Whether the 'advanced' pane is shown when opening the Avatar/Animesh Poser. - Persist - 1 - Type - Boolean - Value - 0 - FSPoserSaveExternalFileAlso Comment @@ -8089,6 +8078,17 @@ Value 0 + FSPoserOnSaveConfirmOverwrite + + Comment + Whether to confirm overwriting a save file. + Persist + 1 + Type + Boolean + Value + 0 + FSPoserStopPosingWhenClosed Comment diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 07e66081e3..bd3bfb70a6 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -61,6 +61,7 @@ constexpr std::string_view POSER_TRACKPAD_SENSITIVITY_SAVE_KEY = "FSPoserTrackpa constexpr std::string_view POSER_STOPPOSINGWHENCLOSED_SAVE_KEY = "FSPoserStopPosingWhenClosed"; constexpr std::string_view POSER_RESETBASEROTONEDIT_SAVE_KEY = "FSPoserResetBaseRotationOnEdit"; constexpr std::string_view POSER_SAVEEXTERNALFORMAT_SAVE_KEY = "FSPoserSaveExternalFileAlso"; +constexpr std::string_view POSER_SAVECONFIRMREQUIRED_SAVE_KEY = "FSPoserOnSaveConfirmOverwrite"; } // namespace /// @@ -178,6 +179,7 @@ bool FSFloaterPoser::postBuild() mBrowserFolderBtn = getChild("open_poseDir_button"); mLoadPosesBtn = getChild("load_poses_button"); mSavePosesBtn = getChild("save_poses_button"); + mSavePosesBtn->setMouseLeaveCallback([this](LLUICtrl*, const LLSD&) { onMouseLeaveSavePoseBtn(); }); mFlipPoseBtn = getChild("FlipPose_avatar"); mFlipJointBtn = getChild("FlipJoint_avatar"); @@ -356,16 +358,22 @@ void FSFloaterPoser::onPoseFileSelect() mPoseSaveNameEditor->setText(name); bool isDeltaSave = !poseFileStartsFromTeePose(name); - if (isDeltaSave) + if (isDeltaSave && hasString("LoadDiffLabel")) mLoadPosesBtn->setLabel(getString("LoadDiffLabel")); - else + else if (hasString("LoadPoseLabel")) mLoadPosesBtn->setLabel(getString("LoadPoseLabel")); } void FSFloaterPoser::onClickPoseSave() { std::string filename = mPoseSaveNameEditor->getValue().asString(); - if (filename.empty()) + if (filename.empty() && hasString("icon_save_failed_button")) + { + mSavePosesBtn->setImageOverlay(getString("icon_save_failed_button"), mSavePosesBtn->getImageOverlayHAlign()); + return; + } + + if (confirmFileOverwrite(filename)) return; LLVOAvatar* avatar = getUiSelectedAvatar(); @@ -381,8 +389,49 @@ void FSFloaterPoser::onClickPoseSave() if (getSavingToBvh()) savePoseToBvh(avatar, filename); - // TODO: provide feedback for save + if (hasString("icon_rotation_is_own_work")) + mSavePosesBtn->setImageOverlay(getString("icon_rotation_is_own_work"), mSavePosesBtn->getImageOverlayHAlign()); + + setSavePosesButtonText(!mPoserAnimator.allBaseRotationsAreZero(avatar)); } + else + { + if (hasString("icon_save_failed_button")) + mSavePosesBtn->setImageOverlay(getString("icon_save_failed_button"), mSavePosesBtn->getImageOverlayHAlign()); + } +} + +bool FSFloaterPoser::confirmFileOverwrite(std::string fileName) +{ + if (fileName.empty()) + return false; + + if (!gSavedSettings.getBOOL(POSER_SAVECONFIRMREQUIRED_SAVE_KEY)) + return false; + + if (!hasString("icon_save_query")) + return false; + + if (mSavePosesBtn->getImageOverlay().notNull() && mSavePosesBtn->getImageOverlay()->getName() == getString("icon_save_query")) + return false; + + std::string fullSavePath = + gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, fileName + POSE_INTERNAL_FORMAT_FILE_EXT); + + if (!gDirUtilp->fileExists(fullSavePath)) + return false; + + mSavePosesBtn->setImageOverlay(getString("icon_save_query"), mSavePosesBtn->getImageOverlayHAlign()); + if (hasString("OverWriteLabel")) + mSavePosesBtn->setLabel(getString("OverWriteLabel")); + + return true; +} + +void FSFloaterPoser::onMouseLeaveSavePoseBtn() +{ + if (hasString("icon_save_button")) + mSavePosesBtn->setImageOverlay(getString("icon_save_button"), mSavePosesBtn->getImageOverlayHAlign()); } void FSFloaterPoser::createUserPoseDirectoryIfNeeded() @@ -2305,7 +2354,8 @@ void FSFloaterPoser::refreshTextHighlightingOnJointScrollLists() void FSFloaterPoser::setSavePosesButtonText(bool setAsSaveDiff) { - setAsSaveDiff ? mSavePosesBtn->setLabel("Save Diff") : mSavePosesBtn->setLabel("Save Pose"); + if (hasString("SavePoseLabel") && hasString("SaveDiffLabel")) + setAsSaveDiff ? mSavePosesBtn->setLabel(getString("SaveDiffLabel")) : mSavePosesBtn->setLabel(getString("SavePoseLabel")); } void FSFloaterPoser::addBoldToScrollList(LLScrollListCtrl* list, LLVOAvatar* avatar) @@ -2600,8 +2650,8 @@ S32 FSFloaterPoser::getBvhJointNegation(const std::string& jointName) const return result; } - bool FSFloaterPoser::getWhetherToResetBaseRotationOnEdit() { return gSavedSettings.getBOOL(POSER_RESETBASEROTONEDIT_SAVE_KEY); } + void FSFloaterPoser::onClickSetBaseRotZero() { mAlsoSaveBvhCbx->setEnabled(getWhetherToResetBaseRotationOnEdit()); } bool FSFloaterPoser::getSavingToBvh() diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 0affb3adb9..93788d57b3 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -220,6 +220,7 @@ public: void createUserPoseDirectoryIfNeeded(); void onToggleLoadSavePanel(); void onClickPoseSave(); + void onMouseLeaveSavePoseBtn(); void onPoseFileSelect(); bool savePoseToXml(LLVOAvatar* avatar, const std::string& posePath); bool savePoseToBvh(LLVOAvatar* avatar, const std::string& posePath); @@ -229,6 +230,7 @@ public: bool poseFileStartsFromTeePose(const std::string& poseFileName); void setPoseSaveFileTextBoxToUiSelectedAvatarSaveFileName(); void setUiSelectedAvatarSaveFileName(const std::string& saveFileName); + bool confirmFileOverwrite(std::string fileName); void startPosingSelf(); void stopPosingAllAvatars(); // visual manipulators control diff --git a/indra/newview/fsposeranimator.h b/indra/newview/fsposeranimator.h index 2276a9a69f..8056ba7785 100644 --- a/indra/newview/fsposeranimator.h +++ b/indra/newview/fsposeranimator.h @@ -504,13 +504,13 @@ public: /// /// Symmetrizes the rotations of the joints from one side of the supplied avatar to the other. /// - /// The avatar whose joints to symmetrizet. + /// The avatar to symmetrize. /// Whether to symmetrize rotations from right to left, otherwise symmetrize left to right. void symmetrizeLeftToRightOrRightToLeft(LLVOAvatar* avatar, bool rightToLeft); /// /// Recaptures the rotation, position and scale state of the supplied joint for the supplied avatar. - /// AsDelta variant retians the original base and creates a delta relative to it. + /// AsDelta variant retains the original base and creates a delta relative to it. /// /// The avatar whose joint is to be recaptured. /// The joint to recapture. @@ -533,14 +533,11 @@ public: void setAllAvatarStartingRotationsToZero(LLVOAvatar* avatar); /// - /// Determines if the kind of save to perform should be a 'delta' save, or a complete save. + /// Determines if the supplied joint has a base rotation of zero. /// - /// The avatar whose pose-rotations are being considered for saving. - /// True if the save should save only 'deltas' to the rotation, otherwise false. - /// - /// A save of the rotation 'deltas' facilitates a user saving their changes to an existing animation. - /// Thus the save represents 'nothing other than the changes the user made', to some other pose which they may have limited rights to. - /// + /// The avatar owning the supplied joint. + /// The joint to query. + /// True if the supplied joint has a 'base' rotation of zero (thus user-supplied change only), otherwise false. bool baseRotationIsZero(LLVOAvatar* avatar, const FSPoserJoint& joint) const; /// 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 1cd3cb1ef1..2cca78dc29 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -11,6 +11,9 @@ width="430"> Inv_Object Check_Mark + Icon_Dock_Foreground + Parcel_Exp_Color + Info tool_tip="When you save your pose, also write a BVH file, which can be uploaded via the 'Build > Upload > Animation' to pose yourself or others in-world. This needs joints to reset their 'base' to zero, because BVH requires original work." top_pad="2" width="134" /> + std::string static vec3ToXYZString(const LLVector3& val); - /// - /// The time when the last click of a button was made. - /// Utilized for controls needing a 'double click do' function. - /// - std::chrono::system_clock::time_point mTimeLastExecutedDoubleClickMethod = std::chrono::system_clock::now(); - - /// - /// The constant time interval, in seconds, a user must execute the notDoubleClicked twice to successfully 'double-click' a button. - /// - std::chrono::duration const mDoubleClickInterval = std::chrono::duration(0.3); - /// /// Unwraps a normalized value from the trackball to a slider value. /// 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 2cca78dc29..77592445a6 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -646,7 +646,7 @@ width="430"> layout="topleft" label="Set Left" name="button_loadHandPoseLeft" - tool_tip="Double click to set your left hand to the selected preset" + tool_tip="Click to set your left hand to the selected preset" top_delta="238" left_pad="1" left="2" @@ -660,7 +660,7 @@ width="430"> layout="topleft" label="Set Right" name="button_loadHandPoseRight" - tool_tip="Double click to set your right hand to the selected preset" + tool_tip="Click to set your right hand to the selected preset" left_pad="2" top_delta="0" width="85" > @@ -828,7 +828,7 @@ width="430"> label="Set to T-Pose" image_unselected="Toolbar_Middle_Off" name="set_t_pose_button" - tool_tip="Double-click to set the selected avatar to a 'T-Pose'" + tool_tip="Click to set the selected avatar to a 'T-Pose'" width="172"> @@ -1123,15 +1123,15 @@ width="430"> + width="53"> @@ -1167,15 +1167,15 @@ width="430"> + width="53"> @@ -1211,15 +1211,15 @@ width="430"> + width="53"> @@ -1404,7 +1404,7 @@ width="430"> image_overlay="Inv_TrashOpen" image_unselected="Toolbar_Middle_Off" name="poser_joint_reset" - tool_tip="Double click to reset the selected body part(s) to when you first started posing" + tool_tip="Click to reset the selected body part(s) to when you first started posing" width="18" top_delta="0" left_pad="1"> @@ -1472,7 +1472,7 @@ width="430"> layout="topleft" label="Copy L > R" name="button_symmetrize_left_to_right" - tool_tip="Double click to copy change from left side to right side." + tool_tip="Click to copy change from left side to right side." left="14" width="70" > layout="topleft" label="Copy R > L" name="button_symmetrize_right_to_left" - tool_tip="Double click to copy change from right side to left side." + tool_tip="Click to copy change from right side to left side." left_pad="23" top_delta="0" width="70" > From 1d962980892d24450ed81d9e0b7acce9c78eefa1 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Thu, 3 Apr 2025 11:52:55 +0100 Subject: [PATCH 06/13] Poser: simplify trackpad rotation changes remove deprecated method and obsolete comments --- indra/newview/fsfloaterposer.cpp | 68 ++++++-------------------------- 1 file changed, 13 insertions(+), 55 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index e603fac9e2..0f41b303e4 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -1771,56 +1771,28 @@ void FSFloaterPoser::onScaleSet() void FSFloaterPoser::onTrackballChanged() { - LLVector3 trackPadPos, trackPadDeltaPos; - LLSD position = mAvatarTrackball->getValue(); + LLVector3 trackPadDeltaPos; LLSD deltaPosition = mAvatarTrackball->getValueDelta(); - if (position.isArray() && position.size() == 3 && deltaPosition.isArray() && deltaPosition.size() == 3) - { - trackPadPos.setValue(position); + if (deltaPosition.isArray() && deltaPosition.size() == 3) trackPadDeltaPos.setValue(deltaPosition); - } else return; F32 trackPadSensitivity = llmax(gSavedSettings.getF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY), 0.0001f); - trackPadPos.mV[VX] *= trackPadSensitivity; - trackPadPos.mV[VY] *= trackPadSensitivity; + trackPadDeltaPos[VX] *= NormalTrackpadRangeInRads * trackPadSensitivity * RAD_TO_DEG; + trackPadDeltaPos[VY] *= NormalTrackpadRangeInRads * trackPadSensitivity * RAD_TO_DEG; + trackPadDeltaPos[VZ] *= NormalTrackpadRangeInRads * RAD_TO_DEG; - trackPadPos.mV[VX] = unWrapScale(trackPadPos.mV[VX]) * NormalTrackpadRangeInRads; - trackPadPos.mV[VY] = unWrapScale(trackPadPos.mV[VY]) * NormalTrackpadRangeInRads; - trackPadPos.mV[VZ] = unWrapScale(trackPadPos.mV[VZ]) * NormalTrackpadRangeInRads; + F32 axis1 = (F32)mYawSpnr->getValue().asReal(); + F32 axis2 = (F32)mPitchSpnr->getValue().asReal(); + F32 axis3 = (F32)mRollSpnr->getValue().asReal(); + mYawSpnr->setValue(axis1 + trackPadDeltaPos[VX]); + mPitchSpnr->setValue(axis2 + trackPadDeltaPos[VY]); + mRollSpnr->setValue(axis3 + trackPadDeltaPos[VZ]); - trackPadDeltaPos[VX] *= NormalTrackpadRangeInRads * trackPadSensitivity; - trackPadDeltaPos[VY] *= NormalTrackpadRangeInRads * trackPadSensitivity; - trackPadDeltaPos[VZ] *= NormalTrackpadRangeInRads; - - setSelectedJointsRotation(trackPadPos, trackPadDeltaPos); - - // WARNING! - // as tempting as it is to refactor the following to refreshRotationSliders(), don't. - // getRotationOfFirstSelectedJoint/setSelectedJointsRotation are - // not necessarily symmetric functions (see their remarks). - mYawSpnr->setValue(trackPadPos.mV[VX] *= RAD_TO_DEG); - mPitchSpnr->setValue(trackPadPos.mV[VY] *= RAD_TO_DEG); - mRollSpnr->setValue(trackPadPos.mV[VZ] *= RAD_TO_DEG); - - enableOrDisableRedoAndUndoButton(); -} - -F32 FSFloaterPoser::unWrapScale(F32 scale) -{ - if (scale > -1.f && scale < 1.f) - return scale; - - F32 result = fmodf(scale, 100.f); // to avoid time consuming while loops - while (result > 1) - result -= 2; - while (result < -1) - result += 2; - - return result; + onYawPitchRollChanged(); } void FSFloaterPoser::onYawPitchRollChanged() @@ -1834,21 +1806,7 @@ void FSFloaterPoser::onYawPitchRollChanged() mLastSliderRotation = absoluteRotation; setSelectedJointsRotation(absoluteRotation, deltaRotation); - - // WARNING! - // as tempting as it is to refactor the following to refreshTrackpadCursor(), don't. - // getRotationOfFirstSelectedJoint/setSelectedJointsRotation are - // not necessarily symmetric functions (see their remarks). - F32 trackPadSensitivity = llmax(gSavedSettings.getF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY), 0.0001f); - absoluteRotation.mV[VX] /= trackPadSensitivity; - absoluteRotation.mV[VY] /= trackPadSensitivity; - - absoluteRotation.mV[VX] /= NormalTrackpadRangeInRads; - absoluteRotation.mV[VY] /= NormalTrackpadRangeInRads; - absoluteRotation.mV[VZ] /= NormalTrackpadRangeInRads; - - mAvatarTrackball->setValue(absoluteRotation.getValue()); - + refreshTrackpadCursor(); enableOrDisableRedoAndUndoButton(); } From a1871912601ac32ed4864ef9eb532426d58f6ee6 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Fri, 4 Apr 2025 11:07:54 +0100 Subject: [PATCH 07/13] Poser: tidy up UI methods and correct shift/alt behaviour when using trackpad --- indra/newview/fsfloaterposer.cpp | 47 ++++++++++++++++++++++---------- indra/newview/fsfloaterposer.h | 14 +++++----- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 0f41b303e4..14a5e2cad9 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -432,6 +432,12 @@ void FSFloaterPoser::onMouseLeaveSavePoseBtn() { if (hasString("icon_save_button")) mSavePosesBtn->setImageOverlay(getString("icon_save_button"), mSavePosesBtn->getImageOverlayHAlign()); + + LLVOAvatar* avatar = getUiSelectedAvatar(); + if (!avatar) + return; + + setSavePosesButtonText(!mPoserAnimator.allBaseRotationsAreZero(avatar)); } void FSFloaterPoser::createUserPoseDirectoryIfNeeded() @@ -1785,17 +1791,29 @@ void FSFloaterPoser::onTrackballChanged() trackPadDeltaPos[VY] *= NormalTrackpadRangeInRads * trackPadSensitivity * RAD_TO_DEG; trackPadDeltaPos[VZ] *= NormalTrackpadRangeInRads * RAD_TO_DEG; - F32 axis1 = (F32)mYawSpnr->getValue().asReal(); - F32 axis2 = (F32)mPitchSpnr->getValue().asReal(); - F32 axis3 = (F32)mRollSpnr->getValue().asReal(); - mYawSpnr->setValue(axis1 + trackPadDeltaPos[VX]); - mPitchSpnr->setValue(axis2 + trackPadDeltaPos[VY]); - mRollSpnr->setValue(axis3 + trackPadDeltaPos[VZ]); + F32 axis1 = clipRange((F32)mYawSpnr->getValue().asReal() + trackPadDeltaPos[VX]); + F32 axis2 = (F32)mPitchSpnr->getValue().asReal() + trackPadDeltaPos[VY]; + F32 axis3 = (F32)mRollSpnr->getValue().asReal() + trackPadDeltaPos[VZ]; - onYawPitchRollChanged(); + mYawSpnr->setValue(axis1); + mPitchSpnr->setValue(axis2); + mRollSpnr->setValue(axis3); + + onYawPitchRollChanged(true); } -void FSFloaterPoser::onYawPitchRollChanged() +F32 FSFloaterPoser::clipRange(F32 value) +{ + F32 result = fmodf(value, 3600.f); // to avoid time consuming while loops + while (result > 180.f) + result -= 360.f; + while (result < -180.f) + result += 360.f; + + return result; +} + +void FSFloaterPoser::onYawPitchRollChanged(bool skipUpdateTrackpad) { LLVector3 absoluteRotation, deltaRotation; absoluteRotation.mV[VX] = (F32)mYawSpnr->getValue().asReal() * DEG_TO_RAD; @@ -1806,8 +1824,10 @@ void FSFloaterPoser::onYawPitchRollChanged() mLastSliderRotation = absoluteRotation; setSelectedJointsRotation(absoluteRotation, deltaRotation); - refreshTrackpadCursor(); enableOrDisableRedoAndUndoButton(); + + if (!skipUpdateTrackpad) + refreshTrackpadCursor(); } void FSFloaterPoser::onAdjustTrackpadSensitivity() @@ -1817,13 +1837,10 @@ void FSFloaterPoser::onAdjustTrackpadSensitivity() void FSFloaterPoser::refreshTrackpadCursor() { - F32 axis1 = (F32)mYawSpnr->getValue().asReal() * DEG_TO_RAD / NormalTrackpadRangeInRads; - F32 axis2 = (F32)mPitchSpnr->getValue().asReal() * DEG_TO_RAD / NormalTrackpadRangeInRads; - F32 axis3 = (F32)mRollSpnr->getValue().asReal() * DEG_TO_RAD / NormalTrackpadRangeInRads; - F32 trackPadSensitivity = llmax(gSavedSettings.getF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY), 0.0001f); - axis1 /= trackPadSensitivity; - axis2 /= trackPadSensitivity; + F32 axis1 = (F32)mYawSpnr->getValue().asReal() * DEG_TO_RAD / NormalTrackpadRangeInRads / trackPadSensitivity; + F32 axis2 = (F32)mPitchSpnr->getValue().asReal() * DEG_TO_RAD / NormalTrackpadRangeInRads / trackPadSensitivity; + F32 axis3 = (F32)mRollSpnr->getValue().asReal() * DEG_TO_RAD / NormalTrackpadRangeInRads; mAvatarTrackball->setValue(axis1, axis2, axis3); } diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index bf4b9291eb..33d32589a1 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -251,7 +251,7 @@ public: void onSetAvatarToTpose(); void onPoseStartStop(); void onTrackballChanged(); - void onYawPitchRollChanged(); + void onYawPitchRollChanged(bool skipUpdateTrackpad = false); void onPositionSet(); void onScaleSet(); void onClickToggleSelectedBoneEnabled(); @@ -427,15 +427,15 @@ public: std::string static vec3ToXYZString(const LLVector3& val); /// - /// Unwraps a normalized value from the trackball to a slider value. + /// Performs an angle module of the supplied value to between -180 & 180 (degrees). /// - /// The scale value from the trackball. - /// A value appropriate for fitting a slider. + /// The value to modulo. + /// The modulo value. /// - /// If the trackpad is in 'infinite scroll' mode, it can produce normalized-values outside the range of the sliders. - /// This method ensures whatever value the trackpad produces, they work with the sliders. + /// If the trackpad is in 'infinite scroll' mode, it can produce normalized-values outside the range of the spinners. + /// This method ensures whatever value the trackpad produces, they work with the spinners. /// - static F32 unWrapScale(F32 scale); + static F32 clipRange(F32 value); LLToolset* mLastToolset{ nullptr }; LLTool* mJointRotTool{ nullptr }; From 983a0637dc16bde940f85e8b15a3a5a4dcbac3e9 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sun, 6 Apr 2025 22:23:47 +0100 Subject: [PATCH 08/13] Poser: first pass at reinstating rotation sliders --- indra/newview/fsfloaterposer.cpp | 37 +++- indra/newview/fsfloaterposer.h | 3 + .../skins/default/xui/en/floater_fs_poser.xml | 170 ++++++++++++------ 3 files changed, 151 insertions(+), 59 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 14a5e2cad9..5a5f04c7e6 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -165,6 +165,10 @@ bool FSFloaterPoser::postBuild() mPosYSlider = getChild("av_position_leftright"); mPosZSlider = getChild("av_position_updown"); + mAdvRotXSlider = getChild("limb_pitch_slider"); + mAdvRotYSlider = getChild("limb_yaw_slider"); + mAdvRotZSlider = getChild("limb_roll_slider"); + mAdvPosXSlider = getChild("Advanced_Position_X"); mAdvPosYSlider = getChild("Advanced_Position_Y"); mAdvPosZSlider = getChild("Advanced_Position_Z"); @@ -797,6 +801,25 @@ void FSFloaterPoser::onCommitSlider(const LLUICtrl* slider, const S32 id) break; } + case 4: // limb_pitch_slider + { + mPitchSpnr->setValue(value); + onYawPitchRollChanged(); + break; + } + case 5: // limb_yaw_slider + { + mYawSpnr->setValue(value); + onYawPitchRollChanged(); + break; + } + case 6: // limb_roll_slider + { + mRollSpnr->setValue(value); + onYawPitchRollChanged(); + break; + } + case 10: // Advanced_Scale_X { mScaleXSpnr->setValue(value); @@ -1816,10 +1839,15 @@ F32 FSFloaterPoser::clipRange(F32 value) void FSFloaterPoser::onYawPitchRollChanged(bool skipUpdateTrackpad) { LLVector3 absoluteRotation, deltaRotation; - absoluteRotation.mV[VX] = (F32)mYawSpnr->getValue().asReal() * DEG_TO_RAD; - absoluteRotation.mV[VY] = (F32)mPitchSpnr->getValue().asReal() * DEG_TO_RAD; - absoluteRotation.mV[VZ] = (F32)mRollSpnr->getValue().asReal() * DEG_TO_RAD; + absoluteRotation.mV[VX] = (F32)mYawSpnr->getValue().asReal(); + absoluteRotation.mV[VY] = (F32)mPitchSpnr->getValue().asReal(); + absoluteRotation.mV[VZ] = (F32)mRollSpnr->getValue().asReal(); + mAdvRotXSlider->setValue(absoluteRotation.mV[VY]); + mAdvRotYSlider->setValue(absoluteRotation.mV[VX]); + mAdvRotZSlider->setValue(absoluteRotation.mV[VZ]); + + absoluteRotation *= DEG_TO_RAD; deltaRotation = absoluteRotation - mLastSliderRotation; mLastSliderRotation = absoluteRotation; @@ -1853,6 +1881,9 @@ void FSFloaterPoser::refreshRotationSlidersAndSpinners() mYawSpnr->setValue(rotation.mV[VX] *= RAD_TO_DEG); mPitchSpnr->setValue(rotation.mV[VY] *= RAD_TO_DEG); mRollSpnr->setValue(rotation.mV[VZ] *= RAD_TO_DEG); + mAdvRotXSlider->setValue(rotation.mV[VY]); + mAdvRotYSlider->setValue(rotation.mV[VX]); + mAdvRotZSlider->setValue(rotation.mV[VZ]); } void FSFloaterPoser::refreshPositionSlidersAndSpinners() diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 33d32589a1..622577b0de 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -448,6 +448,9 @@ public: LLSliderCtrl* mPosXSlider{ nullptr }; LLSliderCtrl* mPosYSlider{ nullptr }; LLSliderCtrl* mPosZSlider{ nullptr }; + LLSliderCtrl* mAdvRotXSlider{ nullptr }; + LLSliderCtrl* mAdvRotYSlider{ nullptr }; + LLSliderCtrl* mAdvRotZSlider{ nullptr }; LLSliderCtrl* mAdvPosXSlider{ nullptr }; LLSliderCtrl* mAdvPosYSlider{ nullptr }; LLSliderCtrl* mAdvPosZSlider{ nullptr }; 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 77592445a6..7312d08578 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -975,7 +975,7 @@ width="430"> width="215"> name="limb_rotation" follows="left|right|top" top="2" - height="170" - width="170" - left="10" + height="160" + width="160" + left="15" tool_tip="Change the rotation of the currently selected body part(s). Hold Ctrl to move slow. Roll the wheel to adjust the 3rd axis. Use Shift or Alt to swap which rotations change" pinch_mode="false" infinite_scroll_mode="true"/> @@ -1011,75 +1011,132 @@ width="430"> name="limb_pitch_label" height="10" layout="topleft" - left="2" + left="5" top_pad="2" - width="57"> + width="200"> Up/Down: + + + + + + + left="5" + top_pad="2" + width="200"> Left/Right: + + + + + + Roll: + + + + height="16" + decimal_digits="2" + follows="left|top" + increment="0.01" + top_pad="-19" + left_pad="0" + name="limb_roll_spinner" + min_val="-180" + max_val="180" + width="53"> - - - - - - + function="Poser.CommitSpinner" + parameter="6"/>