From 47a2a780897fc3560e8acc22152006120e66a729 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sun, 9 Mar 2025 23:22:28 +0000 Subject: [PATCH 1/9] Poser: Refactor undo to one stack Rework UI: removed 'advanced' panel. Remove yaw/pitch/roll sliders. Added Symmetrize L/R. --- indra/newview/fsfloaterposer.cpp | 344 +---- indra/newview/fsfloaterposer.h | 27 +- indra/newview/fsjointpose.cpp | 132 +- indra/newview/fsjointpose.h | 163 ++- indra/newview/fsposeranimator.cpp | 279 ++-- indra/newview/fsposeranimator.h | 66 +- indra/newview/fsposingmotion.cpp | 2 +- .../skins/default/xui/en/floater_fs_poser.xml | 1132 ++++++++--------- 8 files changed, 833 insertions(+), 1312 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 9b0d95ab55..1812a85bdb 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -55,7 +55,6 @@ constexpr char XML_LIST_TITLE_STRING_PREFIX[] = "title_"; constexpr char XML_JOINT_TRANSFORM_STRING_PREFIX[] = "joint_transform_"; constexpr char XML_JOINT_DELTAROT_STRING_PREFIX[] = "joint_delta_rotate_"; constexpr char BVH_JOINT_TRANSFORM_STRING_PREFIX[] = "bvh_joint_transform_"; -constexpr std::string_view POSER_ADVANCEDWINDOWSTATE_SAVE_KEY = "FSPoserAdvancedWindowState"; constexpr std::string_view POSER_TRACKPAD_SENSITIVITY_SAVE_KEY = "FSPoserTrackpadSensitivity"; constexpr std::string_view POSER_STOPPOSINGWHENCLOSED_SAVE_KEY = "FSPoserStopPosingWhenClosed"; constexpr std::string_view POSER_RESETBASEROTONEDIT_SAVE_KEY = "FSPoserResetBaseRotationOnEdit"; @@ -74,10 +73,9 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) mCommitCallbackRegistrar.add("Poser.RefreshAvatars", [this](LLUICtrl*, const LLSD&) { onAvatarsRefresh(); }); mCommitCallbackRegistrar.add("Poser.StartStopAnimating", [this](LLUICtrl*, const LLSD&) { onPoseStartStop(); }); mCommitCallbackRegistrar.add("Poser.ToggleLoadSavePanel", [this](LLUICtrl*, const LLSD&) { onToggleLoadSavePanel(); }); - mCommitCallbackRegistrar.add("Poser.ToggleAdvancedPanel", [this](LLUICtrl*, const LLSD&) { onToggleAdvancedPanel(); }); - mCommitCallbackRegistrar.add("Poser.UndoLastRotation", [this](LLUICtrl*, const LLSD&) { onUndoLastRotation(); }); - mCommitCallbackRegistrar.add("Poser.RedoLastRotation", [this](LLUICtrl*, const LLSD&) { onRedoLastRotation(); }); + mCommitCallbackRegistrar.add("Poser.UndoLastRotation", [this](LLUICtrl*, const LLSD&) { onUndoLastChange(); }); + mCommitCallbackRegistrar.add("Poser.RedoLastRotation", [this](LLUICtrl*, const LLSD&) { onRedoLastChange(); }); mCommitCallbackRegistrar.add("Poser.ToggleMirrorChanges", [this](LLUICtrl*, const LLSD&) { onToggleMirrorChange(); }); mCommitCallbackRegistrar.add("Poser.ToggleSympatheticChanges", [this](LLUICtrl*, const LLSD&) { onToggleSympatheticChange(); }); mCommitCallbackRegistrar.add("Poser.AdjustTrackPadSensitivity", [this](LLUICtrl*, const LLSD&) { onAdjustTrackpadSensitivity(); }); @@ -87,12 +85,9 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) mCommitCallbackRegistrar.add("Poser.Advanced.PositionSet", [this](LLUICtrl*, const LLSD&) { onAdvancedPositionSet(); }); mCommitCallbackRegistrar.add("Poser.Advanced.ScaleSet", [this](LLUICtrl*, const LLSD&) { onAdvancedScaleSet(); }); - mCommitCallbackRegistrar.add("Poser.UndoLastPosition", [this](LLUICtrl*, const LLSD&) { onUndoLastPosition(); }); - mCommitCallbackRegistrar.add("Poser.RedoLastPosition", [this](LLUICtrl*, const LLSD&) { onRedoLastPosition(); }); - mCommitCallbackRegistrar.add("Poser.ResetPosition", [this](LLUICtrl*, const LLSD&) { onResetPosition(); }); - mCommitCallbackRegistrar.add("Poser.ResetScale", [this](LLUICtrl*, const LLSD&) { onResetScale(); }); - mCommitCallbackRegistrar.add("Poser.UndoLastScale", [this](LLUICtrl*, const LLSD&) { onUndoLastScale(); }); - mCommitCallbackRegistrar.add("Poser.RedoLastScale", [this](LLUICtrl*, const LLSD&) { onRedoLastScale(); }); + 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); }); mCommitCallbackRegistrar.add("Poser.Save", [this](LLUICtrl*, const LLSD&) { onClickPoseSave(); }); mCommitCallbackRegistrar.add("Pose.Menu", [this](LLUICtrl*, const LLSD& data) { onPoseMenuAction(data); }); @@ -104,10 +99,9 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) mCommitCallbackRegistrar.add("Poser.FlipJoint", [this](LLUICtrl*, const LLSD&) { onClickFlipSelectedJoints(); }); mCommitCallbackRegistrar.add("Poser.RecaptureSelectedBones", [this](LLUICtrl*, const LLSD&) { onClickRecaptureSelectedBones(); }); mCommitCallbackRegistrar.add("Poser.TogglePosingSelectedBones", [this](LLUICtrl*, const LLSD&) { onClickToggleSelectedBoneEnabled(); }); - mCommitCallbackRegistrar.add("Poser.PoseJointsReset", [this](LLUICtrl*, const LLSD&) { onPoseJointsReset(); }); - //mCommitCallbackRegistrar.add("Poser.CommitSpinner", [this](LLUICtrl* spinnerControl, const LLSD&) { onCommitSpinner(spinnerControl); }); - mCommitCallbackRegistrar.add("Poser.CommitSpinner", boost::bind(&FSFloaterPoser::onCommitSpinner, this, _1, _2)); + mCommitCallbackRegistrar.add("Poser.CommitSpinner", [this](LLUICtrl* spinner, const LLSD& data) { onCommitSpinner(spinner, data); }); + mCommitCallbackRegistrar.add("Poser.Symmetrize", [this](LLUICtrl*, const LLSD& data) { onClickSymmetrize(data); }); } bool FSFloaterPoser::postBuild() @@ -115,15 +109,6 @@ bool FSFloaterPoser::postBuild() mAvatarTrackball = getChild("limb_rotation"); mAvatarTrackball->setCommitCallback([this](LLUICtrl *, const LLSD &) { onLimbTrackballChanged(); }); - mLimbYawSlider = getChild("limb_yaw"); - mLimbYawSlider->setCommitCallback([this](LLUICtrl *, const LLSD &) { onYawPitchRollSliderChanged(); }); - - mLimbPitchSlider = getChild("limb_pitch"); - mLimbPitchSlider->setCommitCallback([this](LLUICtrl *, const LLSD &) { onYawPitchRollSliderChanged(); }); - - mLimbRollSlider = getChild("limb_roll"); - mLimbRollSlider->setCommitCallback([this](LLUICtrl *, const LLSD &) { onYawPitchRollSliderChanged(); }); - mJointsTabs = getChild("joints_tabs"); mJointsTabs->setCommitCallback( [this](LLUICtrl*, const LLSD&) @@ -162,10 +147,6 @@ bool FSFloaterPoser::postBuild() mPosesScrollList->setCommitOnSelectionChange(true); mPosesScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onPoseFileSelect(); }); - mToggleAdvancedPanelBtn = getChild("toggleAdvancedPanel"); - if (gSavedSettings.getBOOL(POSER_ADVANCEDWINDOWSTATE_SAVE_KEY)) - mToggleAdvancedPanelBtn->setValue(true); - mTrackpadSensitivitySlider = getChild("trackpad_sensitivity_slider"); mPoseSaveNameEditor = getChild("pose_save_name"); @@ -204,7 +185,6 @@ bool FSFloaterPoser::postBuild() mSetToTposeButton = getChild("set_t_pose_button"); mJointsParentPnl = getChild("joints_parent_panel"); - mAdvancedParentPnl = getChild("advanced_parent_panel"); mTrackballPnl = getChild("trackball_panel"); mPositionRotationPnl = getChild("positionRotation_panel"); mBodyJointsPnl = getChild("body_joints_panel"); @@ -220,8 +200,11 @@ bool FSFloaterPoser::postBuild() mTrackpadSensitivitySpnr = getChild("trackpad_sensitivity_spinner"); mYawSpnr = getChild("limb_yaw_spinner"); + mYawSpnr->setCommitCallback([this](LLUICtrl*, const LLSD&) { onYawPitchRollChanged(); }); mPitchSpnr = getChild("limb_pitch_spinner"); - mRollSpnr = getChild("limb_roll_spinner"); + mPitchSpnr->setCommitCallback([this](LLUICtrl*, const LLSD&) { onYawPitchRollChanged(); }); + mRollSpnr = getChild("limb_roll_spinner"); + mRollSpnr->setCommitCallback([this](LLUICtrl*, const LLSD&) { onYawPitchRollChanged(); }); mUpDownSpnr = getChild("av_position_updown_spinner"); mLeftRightSpnr = getChild("av_position_leftright_spinner"); mInOutSpnr = getChild("av_position_inout_spinner"); @@ -241,7 +224,6 @@ void FSFloaterPoser::onOpen(const LLSD& key) onAvatarsRefresh(); refreshJointScrollListMembers(); onJointTabSelect(); - onOpenSetAdvancedPanel(); refreshPoseScroll(mHandPresetsScrollList, POSE_PRESETS_HANDS_SUBDIRECTORY); startPosingSelf(); @@ -250,9 +232,6 @@ void FSFloaterPoser::onOpen(const LLSD& key) void FSFloaterPoser::onClose(bool app_quitting) { - if (mToggleAdvancedPanelBtn) - gSavedSettings.setBOOL(POSER_ADVANCEDWINDOWSTATE_SAVE_KEY, mToggleAdvancedPanelBtn->getValue().asBoolean()); - if (gSavedSettings.getBOOL(POSER_STOPPOSINGWHENCLOSED_SAVE_KEY)) stopPosingAllAvatars(); @@ -582,8 +561,24 @@ void FSFloaterPoser::onClickBrowsePoseCache() gViewerWindow->getWindow()->openFile(pathname); } -//void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner) -// Pass in an ID as a parameter, so you can use a switch statement +void FSFloaterPoser::onClickSymmetrize(S32 ID) +{ + if (notDoubleClicked()) + return; + + LLVOAvatar* avatar = getUiSelectedAvatar(); + if (!avatar) + return; + + if (!mPoserAnimator.isPosingAvatar(avatar)) + return; + + mPoserAnimator.symmetrizeLeftToRightOrRightToLeft(avatar, ID == 2); + + refreshRotationSlidersAndSpinners(); + refreshTrackpadCursor(); +} + void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner, S32 id) { if (!spinner) @@ -597,8 +592,6 @@ void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner, S32 id) F32 value = (F32)spinner->getValue().asReal(); - // Use the ID passed in to perform a switch statment - // which should make each action take the same amount of time. switch (id) { case 0: // av_position_updown_spinner @@ -624,24 +617,6 @@ void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner, S32 id) onAdjustTrackpadSensitivity(); break; } - case 4: // limb_pitch_spinner - { - mLimbPitchSlider->setValue(value); - onYawPitchRollSliderChanged(); - break; - } - case 5: // limb_yaw_spinner - { - mLimbYawSlider->setValue(value); - onYawPitchRollSliderChanged(); - break; - } - case 6: // limb_roll_spinner - { - mLimbRollSlider->setValue(value); - onYawPitchRollSliderChanged(); - break; - } case 7: // adv_posx_spinner { if (changingBodyPosition) @@ -690,34 +665,6 @@ void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner, S32 id) } } -void FSFloaterPoser::onPoseJointsReset() -{ - if (notDoubleClicked()) - return; - - LLVOAvatar* avatar = getUiSelectedAvatar(); - if (!avatar) - return; - - if (!mPoserAnimator.isPosingAvatar(avatar)) - return; - - auto selectedJoints = getUiSelectedPoserJoints(); - if (selectedJoints.size() < 1) - return; - - for (auto item : selectedJoints) - { - bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - mPoserAnimator.resetAvatarJoint(avatar, *item); - } - - refreshRotationSlidersAndSpinners(); - refreshTrackpadCursor(); - refreshAvatarPositionSlidersAndSpinners(); -} - void FSFloaterPoser::onPoseMenuAction(const LLSD& param) { std::string loadStyle = param.asString(); @@ -1018,7 +965,7 @@ void FSFloaterPoser::startPosingSelf() void FSFloaterPoser::stopPosingAllAvatars() { - if (!gAgentAvatarp || gAgentAvatarp.isNull()) + if (!gAgentAvatarp || gAgentAvatarp.isNull() || !mAvatarSelectionScrollList) return; for (auto listItem : mAvatarSelectionScrollList->getAllData()) @@ -1087,7 +1034,6 @@ bool FSFloaterPoser::havePermissionToAnimateAvatar(LLVOAvatar *avatar) const void FSFloaterPoser::poseControlsEnable(bool enable) { - mAdvancedParentPnl->setEnabled(enable); mTrackballPnl->setEnabled(enable); mFlipPoseBtn->setEnabled(enable); mFlipJointBtn->setEnabled(enable); @@ -1254,7 +1200,7 @@ void FSFloaterPoser::setRotationChangeButtons(bool togglingMirror, bool toggling refreshTrackpadCursor(); } -void FSFloaterPoser::onUndoLastRotation() +void FSFloaterPoser::onUndoLastChange() { LLVOAvatar* avatar = getUiSelectedAvatar(); if (!avatar) @@ -1271,59 +1217,15 @@ void FSFloaterPoser::onUndoLastRotation() { bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); if (currentlyPosing) - mPoserAnimator.undoLastJointRotation(avatar, *item, getUiSelectedBoneDeflectionStyle()); + mPoserAnimator.undoLastJointChange(avatar, *item, getUiSelectedBoneDeflectionStyle()); } enableOrDisableRedoButton(); refreshRotationSlidersAndSpinners(); refreshTrackpadCursor(); -} - -void FSFloaterPoser::onUndoLastPosition() -{ - LLVOAvatar* avatar = getUiSelectedAvatar(); - if (!avatar) - return; - - if (!mPoserAnimator.isPosingAvatar(avatar)) - return; - - auto selectedJoints = getUiSelectedPoserJoints(); - if (selectedJoints.size() < 1) - return; - - for (auto item : selectedJoints) - { - bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - mPoserAnimator.undoLastJointPosition(avatar, *item, getUiSelectedBoneDeflectionStyle()); - } - - refreshAdvancedPositionSlidersAndSpinners(); + refreshPositionSlidersAndSpinners(); refreshAvatarPositionSlidersAndSpinners(); -} - -void FSFloaterPoser::onUndoLastScale() -{ - LLVOAvatar* avatar = getUiSelectedAvatar(); - if (!avatar) - return; - - if (!mPoserAnimator.isPosingAvatar(avatar)) - return; - - auto selectedJoints = getUiSelectedPoserJoints(); - if (selectedJoints.size() < 1) - return; - - for (auto item : selectedJoints) - { - bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - mPoserAnimator.undoLastJointScale(avatar, *item, getUiSelectedBoneDeflectionStyle()); - } - - refreshAdvancedScaleSlidersAndSpinners(); + refreshScaleSlidersAndSpinners(); } void FSFloaterPoser::onSetAvatarToTpose() @@ -1340,11 +1242,13 @@ void FSFloaterPoser::onSetAvatarToTpose() refreshTextHighlightingOnJointScrollLists(); } -void FSFloaterPoser::onResetPosition() +void FSFloaterPoser::onResetJoint(const LLSD data) { if (notDoubleClicked()) return; + int resetType = data.asInteger(); + LLVOAvatar* avatar = getUiSelectedAvatar(); if (!avatar) return; @@ -1359,41 +1263,20 @@ void FSFloaterPoser::onResetPosition() for (auto item : selectedJoints) { bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - mPoserAnimator.resetJointPosition(avatar, *item, getUiSelectedBoneDeflectionStyle()); + if (!currentlyPosing) + continue; + + mPoserAnimator.resetJoint(avatar, *item, getUiSelectedBoneDeflectionStyle()); } - refreshAdvancedPositionSlidersAndSpinners(); + refreshRotationSlidersAndSpinners(); + refreshTrackpadCursor(); refreshAvatarPositionSlidersAndSpinners(); + refreshPositionSlidersAndSpinners(); + refreshScaleSlidersAndSpinners(); } -void FSFloaterPoser::onResetScale() -{ - if (notDoubleClicked()) - return; - - LLVOAvatar* avatar = getUiSelectedAvatar(); - if (!avatar) - return; - - if (!mPoserAnimator.isPosingAvatar(avatar)) - return; - - auto selectedJoints = getUiSelectedPoserJoints(); - if (selectedJoints.size() < 1) - return; - - for (auto item : selectedJoints) - { - bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - mPoserAnimator.resetJointScale(avatar, *item, getUiSelectedBoneDeflectionStyle()); - } - - refreshAdvancedScaleSlidersAndSpinners(); -} - -void FSFloaterPoser::onRedoLastRotation() +void FSFloaterPoser::onRedoLastChange() { LLVOAvatar* avatar = getUiSelectedAvatar(); if (!avatar) @@ -1410,61 +1293,17 @@ void FSFloaterPoser::onRedoLastRotation() { bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); if (currentlyPosing) - mPoserAnimator.redoLastJointRotation(avatar, *item, getUiSelectedBoneDeflectionStyle()); + mPoserAnimator.redoLastJointChange(avatar, *item, getUiSelectedBoneDeflectionStyle()); } enableOrDisableRedoButton(); refreshRotationSlidersAndSpinners(); refreshTrackpadCursor(); -} - -void FSFloaterPoser::onRedoLastPosition() -{ - LLVOAvatar* avatar = getUiSelectedAvatar(); - if (!avatar) - return; - - if (!mPoserAnimator.isPosingAvatar(avatar)) - return; - - auto selectedJoints = getUiSelectedPoserJoints(); - if (selectedJoints.size() < 1) - return; - - for (auto item : selectedJoints) - { - bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - mPoserAnimator.redoLastJointPosition(avatar, *item, getUiSelectedBoneDeflectionStyle()); - } - - refreshAdvancedPositionSlidersAndSpinners(); + refreshScaleSlidersAndSpinners(); + refreshPositionSlidersAndSpinners(); refreshAvatarPositionSlidersAndSpinners(); } -void FSFloaterPoser::onRedoLastScale() -{ - LLVOAvatar* avatar = getUiSelectedAvatar(); - if (!avatar) - return; - - if (!mPoserAnimator.isPosingAvatar(avatar)) - return; - - auto selectedJoints = getUiSelectedPoserJoints(); - if (selectedJoints.size() < 1) - return; - - for (auto item : selectedJoints) - { - bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); - if (currentlyPosing) - mPoserAnimator.redoLastJointScale(avatar, *item, getUiSelectedBoneDeflectionStyle()); - } - - refreshAdvancedScaleSlidersAndSpinners(); -} - void FSFloaterPoser::enableOrDisableRedoButton() { LLVOAvatar* avatar = getUiSelectedAvatar(); @@ -1483,42 +1322,12 @@ void FSFloaterPoser::enableOrDisableRedoButton() { bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); if (currentlyPosing) - shouldEnableRedoButton |= mPoserAnimator.canRedoJointRotation(avatar, *item); + shouldEnableRedoButton |= mPoserAnimator.canRedoJointChange(avatar, *item); } mRedoChangeBtn->setEnabled(shouldEnableRedoButton); } -void FSFloaterPoser::onOpenSetAdvancedPanel() -{ - bool advancedPanelExpanded = mToggleAdvancedPanelBtn->getValue().asBoolean(); - if (advancedPanelExpanded) - onToggleAdvancedPanel(); -} - -void FSFloaterPoser::onToggleAdvancedPanel() -{ - if (isMinimized()) - return; - - bool advancedPanelExpanded = mToggleAdvancedPanelBtn->getValue().asBoolean(); - - mAdvancedParentPnl->setVisible(advancedPanelExpanded); - - // change the height of the Poser panel - S32 currentHeight = getRect().getHeight(); - S32 advancedPanelHeight = mAdvancedParentPnl->getRect().getHeight(); - - S32 poserFloaterHeight = advancedPanelExpanded ? currentHeight + advancedPanelHeight : currentHeight - advancedPanelHeight; - S32 poserFloaterWidth = getRect().getWidth(); - - if (poserFloaterHeight < 0) - return; - - reshape(poserFloaterWidth, poserFloaterHeight); - onJointTabSelect(); -} - std::vector FSFloaterPoser::getUiSelectedPoserJoints() const { std::vector joints; @@ -1733,7 +1542,7 @@ void FSFloaterPoser::onAvatarPositionSet() mUpDownSpnr->setValue(posZ); setSelectedJointsPosition(posX, posY, posZ); - refreshAdvancedPositionSlidersAndSpinners(); + refreshPositionSlidersAndSpinners(); } void FSFloaterPoser::onLimbTrackballChanged() @@ -1769,13 +1578,9 @@ void FSFloaterPoser::onLimbTrackballChanged() // as tempting as it is to refactor the following to refreshRotationSliders(), don't. // getRotationOfFirstSelectedJoint/setSelectedJointsRotation are // not necessarily symmetric functions (see their remarks). - mLimbYawSlider->setValue(trackPadPos.mV[VX] *= RAD_TO_DEG); - mLimbPitchSlider->setValue(trackPadPos.mV[VY] *= RAD_TO_DEG); - mLimbRollSlider->setValue(trackPadPos.mV[VZ] *= RAD_TO_DEG); - - mYawSpnr->setValue(mLimbYawSlider->getValueF32()); - mPitchSpnr->setValue(mLimbPitchSlider->getValueF32()); - mRollSpnr->setValue(mLimbRollSlider->getValueF32()); + mYawSpnr->setValue(trackPadPos.mV[VX] *= RAD_TO_DEG); + mPitchSpnr->setValue(trackPadPos.mV[VY] *= RAD_TO_DEG); + mRollSpnr->setValue(trackPadPos.mV[VZ] *= RAD_TO_DEG); } F32 FSFloaterPoser::unWrapScale(F32 scale) @@ -1792,12 +1597,12 @@ F32 FSFloaterPoser::unWrapScale(F32 scale) return result; } -void FSFloaterPoser::onYawPitchRollSliderChanged() +void FSFloaterPoser::onYawPitchRollChanged() { LLVector3 absoluteRotation, deltaRotation; - absoluteRotation.mV[VX] = mLimbYawSlider->getValueF32() * DEG_TO_RAD; - absoluteRotation.mV[VY] = mLimbPitchSlider->getValueF32() * DEG_TO_RAD; - absoluteRotation.mV[VZ] = mLimbRollSlider->getValueF32() * DEG_TO_RAD; + 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; deltaRotation = absoluteRotation - mLastSliderRotation; mLastSliderRotation = absoluteRotation; @@ -1817,10 +1622,6 @@ void FSFloaterPoser::onYawPitchRollSliderChanged() absoluteRotation.mV[VZ] /= NormalTrackpadRangeInRads; mAvatarTrackball->setValue(absoluteRotation.getValue()); - - mYawSpnr->setValue(mLimbYawSlider->getValueF32()); - mPitchSpnr->setValue(mLimbPitchSlider->getValueF32()); - mRollSpnr->setValue(mLimbRollSlider->getValueF32()); } void FSFloaterPoser::onAdjustTrackpadSensitivity() @@ -1830,9 +1631,9 @@ void FSFloaterPoser::onAdjustTrackpadSensitivity() void FSFloaterPoser::refreshTrackpadCursor() { - F32 axis1 = mLimbYawSlider->getValueF32() * DEG_TO_RAD / NormalTrackpadRangeInRads; - F32 axis2 = mLimbPitchSlider->getValueF32() * DEG_TO_RAD / NormalTrackpadRangeInRads; - F32 axis3 = mLimbRollSlider->getValueF32() * DEG_TO_RAD / NormalTrackpadRangeInRads; + 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; @@ -1867,15 +1668,12 @@ void FSFloaterPoser::refreshRotationSlidersAndSpinners() LLVector3 rotation = getRotationOfFirstSelectedJoint(); mLastSliderRotation = rotation; - mLimbYawSlider->setValue(rotation.mV[VX] *= RAD_TO_DEG); - mYawSpnr->setValue(rotation.mV[VX]); - mLimbPitchSlider->setValue(rotation.mV[VY] *= RAD_TO_DEG); - mPitchSpnr->setValue(rotation.mV[VY]); - mLimbRollSlider->setValue(rotation.mV[VZ] *= RAD_TO_DEG); - mRollSpnr->setValue(rotation.mV[VZ]); + mYawSpnr->setValue(rotation.mV[VX] *= RAD_TO_DEG); + mPitchSpnr->setValue(rotation.mV[VY] *= RAD_TO_DEG); + mRollSpnr->setValue(rotation.mV[VZ] *= RAD_TO_DEG); } -void FSFloaterPoser::refreshAdvancedPositionSlidersAndSpinners() +void FSFloaterPoser::refreshPositionSlidersAndSpinners() { LLVector3 position = getPositionOfFirstSelectedJoint(); @@ -1887,7 +1685,7 @@ void FSFloaterPoser::refreshAdvancedPositionSlidersAndSpinners() mAdvPosZSpnr->setValue(position.mV[VZ]); } -void FSFloaterPoser::refreshAdvancedScaleSlidersAndSpinners() +void FSFloaterPoser::refreshScaleSlidersAndSpinners() { LLVector3 rotation = getScaleOfFirstSelectedJoint(); @@ -2041,16 +1839,12 @@ LLVector3 FSFloaterPoser::getScaleOfFirstSelectedJoint() const void FSFloaterPoser::onJointTabSelect() { refreshAvatarPositionSlidersAndSpinners(); - refreshRotationSlidersAndSpinners(); + refreshRotationSlidersAndSpinners(); refreshTrackpadCursor(); enableOrDisableRedoButton(); + refreshPositionSlidersAndSpinners(); + refreshScaleSlidersAndSpinners(); onClickSetBaseRotZero(); - - if (mToggleAdvancedPanelBtn->getValue().asBoolean()) - { - refreshAdvancedPositionSlidersAndSpinners(); - refreshAdvancedScaleSlidersAndSpinners(); - } } E_BoneAxisTranslation FSFloaterPoser::getJointTranslation(const std::string& jointName) const diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 0dbe809af7..bd17406e7f 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -220,25 +220,19 @@ class FSFloaterPoser : public LLFloater void onAvatarsRefresh(); void onAvatarSelect(); void onJointTabSelect(); - void onToggleAdvancedPanel(); void onToggleMirrorChange(); void onToggleSympatheticChange(); void setRotationChangeButtons(bool mirror, bool sympathetic); - void onUndoLastRotation(); - void onRedoLastRotation(); - void onUndoLastPosition(); - void onRedoLastPosition(); - void onUndoLastScale(); - void onRedoLastScale(); - void onResetPosition(); - void onResetScale(); + void onUndoLastChange(); + void onRedoLastChange(); + void onResetJoint(const LLSD data); void onSetAvatarToTpose(); void enableOrDisableRedoButton(); void onPoseStartStop(); void startPosingSelf(); void stopPosingAllAvatars(); void onLimbTrackballChanged(); - void onYawPitchRollSliderChanged(); + void onYawPitchRollChanged(); void onAvatarPositionSet(); void onAdvancedPositionSet(); void onAdvancedScaleSet(); @@ -246,22 +240,20 @@ class FSFloaterPoser : public LLFloater void onClickRecaptureSelectedBones(); void onClickFlipPose(); void onClickFlipSelectedJoints(); - void onPoseJointsReset(); - void onOpenSetAdvancedPanel(); void onAdjustTrackpadSensitivity(); void onClickLoadLeftHandPose(); void onClickLoadRightHandPose(); void onClickLoadHandPose(bool isRightHand); void onClickSetBaseRotZero(); - //void onCommitSpinner(LLUICtrl* spinner); void onCommitSpinner(LLUICtrl* spinner, S32 ID); + void onClickSymmetrize(S32 ID); // UI Refreshments void refreshRotationSlidersAndSpinners(); void refreshAvatarPositionSlidersAndSpinners(); void refreshTrackpadCursor(); - void refreshAdvancedPositionSlidersAndSpinners(); - void refreshAdvancedScaleSlidersAndSpinners(); + void refreshPositionSlidersAndSpinners(); + void refreshScaleSlidersAndSpinners(); /// /// Determines if we have permission to animate the supplied avatar. @@ -447,9 +439,6 @@ class FSFloaterPoser : public LLFloater FSVirtualTrackpad* mAvatarTrackball{ nullptr }; LLSliderCtrl* mTrackpadSensitivitySlider{ nullptr }; - LLSliderCtrl* mLimbYawSlider{ nullptr }; - LLSliderCtrl* mLimbPitchSlider{ nullptr }; // pointing your nose up or down - LLSliderCtrl* mLimbRollSlider{ nullptr }; // your ear touches your shoulder LLSliderCtrl* mPosXSlider{ nullptr }; LLSliderCtrl* mPosYSlider{ nullptr }; LLSliderCtrl* mPosZSlider{ nullptr }; @@ -473,7 +462,6 @@ class FSFloaterPoser : public LLFloater LLScrollListCtrl* mPosesScrollList{ nullptr }; LLScrollListCtrl* mHandPresetsScrollList{ nullptr }; - LLButton* mToggleAdvancedPanelBtn{ nullptr }; LLButton* mStartStopPosingBtn{ nullptr }; LLButton* mToggleLoadSavePanelBtn{ nullptr }; LLButton* mBrowserFolderBtn{ nullptr }; @@ -491,7 +479,6 @@ class FSFloaterPoser : public LLFloater LLLineEditor* mPoseSaveNameEditor{ nullptr }; - LLPanel* mAdvancedParentPnl{ nullptr }; LLPanel* mJointsParentPnl{ nullptr }; LLPanel* mTrackballPnl{ nullptr }; LLPanel* mPositionRotationPnl{ nullptr }; diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index 285e1c5322..1693331b41 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -30,7 +30,7 @@ #include "llcharacter.h" /// -/// The maximum length of any undo queue; adding new members preens older ones. +/// The maximum length of the undo queue; adding new members preens older ones. /// constexpr size_t MaximumUndoQueueLength = 20; @@ -48,103 +48,87 @@ FSJointPose::FSJointPose(LLJoint* joint, U32 usage, bool isCollisionVolume) mJointName = joint->getName(); mIsCollisionVolume = isCollisionVolume; - mRotation = FSJointRotation(joint->getRotation()); - mBeginningPosition = joint->getPosition(); - mBeginningScale = joint->getScale(); + mCurrentState = FSJointState(joint); + mBeginningState = FSJointState(joint); } -void FSJointPose::setPositionDelta(const LLVector3& pos) +void FSJointPose::setPublicPosition(const LLVector3& pos) { - addToUndo(mPositionDelta, &mUndonePositionIndex, &mLastSetPositionDeltas, &mTimeLastUpdatedPosition); - mPositionDelta.set(pos); + addStateToUndo(FSJointState(mCurrentState)); + mCurrentState.mPosition.set(pos); } -void FSJointPose::setRotationDelta(const LLQuaternion& rot) +void FSJointPose::setPublicRotation(const LLQuaternion& rot) { - addToUndo(mRotation, &mUndoneRotationIndex, &mLastSetRotationDeltas, &mTimeLastUpdatedRotation); - mRotation = FSJointRotation(mRotation.baseRotation, rot); + addStateToUndo(FSJointState(mCurrentState)); + mCurrentState.mRotation.set(rot); } -void FSJointPose::setScaleDelta(const LLVector3& scale) +void FSJointPose::setPublicScale(const LLVector3& scale) { - addToUndo(mScaleDelta, &mUndoneScaleIndex, &mLastSetScaleDeltas, &mTimeLastUpdatedScale); - mScaleDelta.set(scale); + addStateToUndo(FSJointState(mCurrentState)); + mCurrentState.mScale.set(scale); } -void FSJointPose::undoLastPositionChange() +void FSJointPose::undoLastChange() { - mPositionDelta.set(undoLastChange(mPositionDelta, &mUndonePositionIndex, &mLastSetPositionDeltas)); + mCurrentState = undoLastStateChange(FSJointState(mCurrentState)); } -void FSJointPose::undoLastRotationChange() +void FSJointPose::redoLastChange() { - mRotation.set(undoLastChange(mRotation, &mUndoneRotationIndex, &mLastSetRotationDeltas)); + mCurrentState = redoLastStateChange(FSJointState(mCurrentState)); } -void FSJointPose::undoLastScaleChange() { mScaleDelta.set(undoLastChange(mScaleDelta, &mUndoneScaleIndex, &mLastSetScaleDeltas)); } - -void FSJointPose::redoLastPositionChange() +void FSJointPose::addStateToUndo(FSJointState stateToAddToUndo) { - mPositionDelta.set(redoLastChange(mPositionDelta, &mUndonePositionIndex, &mLastSetPositionDeltas)); -} - -void FSJointPose::redoLastRotationChange() -{ - mRotation.set(redoLastChange(mRotation, &mUndoneRotationIndex, &mLastSetRotationDeltas)); -} - -void FSJointPose::redoLastScaleChange() { mScaleDelta.set(redoLastChange(mScaleDelta, &mUndoneScaleIndex, &mLastSetScaleDeltas)); } - -template -inline void FSJointPose::addToUndo(T delta, size_t* undoIndex, std::deque* dequeue, - std::chrono::system_clock::time_point* timeLastUpdated) -{ - auto timeIntervalSinceLastChange = std::chrono::system_clock::now() - *timeLastUpdated; - *timeLastUpdated = std::chrono::system_clock::now(); + auto timeIntervalSinceLastChange = std::chrono::system_clock::now() - mTimeLastUpdatedCurrentState; + mTimeLastUpdatedCurrentState = std::chrono::system_clock::now(); if (timeIntervalSinceLastChange < UndoUpdateInterval) return; - if (*undoIndex > 0) + if (mUndoneJointStatesIndex > 0) { - for (size_t i = 0; i < *undoIndex; i++) - dequeue->pop_front(); + for (size_t i = 0; i <= mUndoneJointStatesIndex; i++) + if (!mLastSetJointStates.empty()) + mLastSetJointStates.pop_front(); - *undoIndex = 0; + mUndoneJointStatesIndex = 0; } - dequeue->push_front(delta); + mLastSetJointStates.push_front(stateToAddToUndo); - while (dequeue->size() > MaximumUndoQueueLength) - dequeue->pop_back(); + while (mLastSetJointStates.size() > MaximumUndoQueueLength) + mLastSetJointStates.pop_back(); } -template T FSJointPose::undoLastChange(T thingToSet, size_t* undoIndex, std::deque* dequeue) +FSJointPose::FSJointState FSJointPose::undoLastStateChange(FSJointState thingToSet) { - if (dequeue->empty()) + if (mLastSetJointStates.empty()) return thingToSet; - if (*undoIndex == 0) - dequeue->push_front(thingToSet); + if (mUndoneJointStatesIndex == 0) + mLastSetJointStates.push_front(thingToSet); - *undoIndex += 1; - *undoIndex = llclamp(*undoIndex, 0, dequeue->size() - 1); + mUndoneJointStatesIndex += 1; + mUndoneJointStatesIndex = llclamp(mUndoneJointStatesIndex, 0, mLastSetJointStates.size() - 1); - return dequeue->at(*undoIndex); + return mLastSetJointStates.at(mUndoneJointStatesIndex); } -template T FSJointPose::redoLastChange(T thingToSet, size_t* undoIndex, std::deque* dequeue) +FSJointPose::FSJointState FSJointPose::redoLastStateChange(FSJointState thingToSet) { - if (dequeue->empty()) + if (mLastSetJointStates.empty()) return thingToSet; - if (*undoIndex == 0) + if (mUndoneJointStatesIndex == 0) return thingToSet; - *undoIndex -= 1; - *undoIndex = llclamp(*undoIndex, 0, dequeue->size() - 1); - T result = dequeue->at(*undoIndex); - if (*undoIndex == 0) - dequeue->pop_front(); + mUndoneJointStatesIndex -= 1; + mUndoneJointStatesIndex = llclamp(mUndoneJointStatesIndex, 0, mLastSetJointStates.size() - 1); + auto result = mLastSetJointStates.at(mUndoneJointStatesIndex); + if (mUndoneJointStatesIndex == 0) + mLastSetJointStates.pop_front(); return result; } @@ -158,8 +142,8 @@ void FSJointPose::recaptureJoint() if (!joint) return; - addToUndo(mRotation, &mUndoneRotationIndex, &mLastSetRotationDeltas, &mTimeLastUpdatedRotation); - mRotation = FSJointRotation(joint->getRotation()); + addStateToUndo(FSJointState(mCurrentState)); + mCurrentState = FSJointState(joint); } void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) @@ -169,9 +153,9 @@ void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) if (mIsCollisionVolume) return; - auto tempRot = FSJointRotation(mRotation); - mRotation = FSJointRotation(oppositeJoint->mRotation); - oppositeJoint->mRotation = tempRot; + auto tempState = FSJointState(mCurrentState); + mCurrentState.cloneRotationFrom(oppositeJoint->mCurrentState); + oppositeJoint->mCurrentState.cloneRotationFrom(tempState); } void FSJointPose::cloneRotationFrom(FSJointPose* fromJoint) @@ -179,8 +163,8 @@ void FSJointPose::cloneRotationFrom(FSJointPose* fromJoint) if (!fromJoint) return; - addToUndo(mRotation, &mUndoneRotationIndex, &mLastSetRotationDeltas, &mTimeLastUpdatedRotation); - mRotation = FSJointRotation(fromJoint->mRotation); + addStateToUndo(FSJointState(mCurrentState)); + mCurrentState.cloneRotationFrom(fromJoint->mCurrentState); } void FSJointPose::mirrorRotationFrom(FSJointPose* fromJoint) @@ -189,11 +173,7 @@ void FSJointPose::mirrorRotationFrom(FSJointPose* fromJoint) return; cloneRotationFrom(fromJoint); - - mRotation.baseRotation = LLQuaternion(-mRotation.baseRotation.mQ[VX], mRotation.baseRotation.mQ[VY], -mRotation.baseRotation.mQ[VZ], - mRotation.baseRotation.mQ[VW]); - mRotation.deltaRotation = LLQuaternion(-mRotation.deltaRotation.mQ[VX], mRotation.deltaRotation.mQ[VY], -mRotation.deltaRotation.mQ[VZ], - mRotation.deltaRotation.mQ[VW]); + mCurrentState.reflectRotation(); } void FSJointPose::revertJoint() @@ -202,9 +182,9 @@ void FSJointPose::revertJoint() if (!joint) return; - joint->setRotation(mRotation.baseRotation); - joint->setPosition(mBeginningPosition); - joint->setScale(mBeginningScale); + joint->setRotation(mBeginningState.getTargetRotation()); + joint->setPosition(mBeginningState.getTargetPosition()); + joint->setScale(mBeginningState.getTargetScale()); } void FSJointPose::reflectRotation() @@ -212,7 +192,7 @@ void FSJointPose::reflectRotation() if (mIsCollisionVolume) return; - mRotation.reflectRotation(); + mCurrentState.reflectRotation(); } void FSJointPose::zeroBaseRotation() @@ -220,7 +200,7 @@ void FSJointPose::zeroBaseRotation() if (mIsCollisionVolume) return; - mRotation.baseRotation = LLQuaternion::DEFAULT; + mBeginningState.mRotation = LLQuaternion::DEFAULT; } bool FSJointPose::isBaseRotationZero() const @@ -228,5 +208,5 @@ bool FSJointPose::isBaseRotationZero() const if (mIsCollisionVolume) return true; - return mRotation.baseRotation == LLQuaternion::DEFAULT; + return mBeginningState.mRotation == LLQuaternion::DEFAULT; } diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index 1b50f908bd..b521458efe 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -58,34 +58,39 @@ class FSJointPose bool isCollisionVolume() const { return mIsCollisionVolume; } /// - /// Gets the position change the animator wishes the joint to have. + /// Gets the 'public' position of the joint. /// - LLVector3 getPositionDelta() const { return mPositionDelta; } + LLVector3 getPublicPosition() const { return mCurrentState.mPosition; } /// - /// Sets the position the animator wishes the joint to be in. + /// Sets the 'public' position of the joint. /// - void setPositionDelta(const LLVector3& pos); + void setPublicPosition(const LLVector3& pos); /// /// Undoes the last position set, if any. /// - void undoLastPositionChange(); + void undoLastChange(); /// /// Undoes the last position set, if any. /// - void redoLastPositionChange(); + void redoLastChange(); /// - /// Gets the rotation the animator wishes the joint to be in. + /// Gets the 'public' rotation of the joint. /// - LLQuaternion getRotationDelta() const { return mRotation.deltaRotation; } + LLQuaternion getPublicRotation() const { return mCurrentState.mRotation; } /// - /// Sets the rotation the animator wishes the joint to be in. + /// Sets the 'public' rotation of the joint. /// - void setRotationDelta(const LLQuaternion& rot); + /// + /// 'Public rotation' is the amount of rotation the user has added to the initial state. + /// Public rotation is what a user may save to an external format (such as BVH). + /// This distinguishes 'private' rotation, which is the state inherited from something like a pose in-world. + /// + void setPublicRotation(const LLQuaternion& rot); /// /// Reflects the base and delta rotation of the represented joint left-right. @@ -93,7 +98,7 @@ class FSJointPose void reflectRotation(); /// - /// Sets the base rotation of the represented joint to zero. + /// Sets the private rotation of the represented joint to zero. /// void zeroBaseRotation(); @@ -104,43 +109,20 @@ class FSJointPose bool isBaseRotationZero() const; /// - /// Undoes the last rotation set, if any. - /// Ordinarily the queue does not contain the current rotation, because we rely on time to add, and not button-up. - /// When we undo, if we are at the top of the queue, we need to add the current rotation so we can redo back to it. - /// Thus when we start undoing, mUndoneRotationIndex points at the current rotation. + /// Gets whether a redo of this joint may be performed. /// - void undoLastRotationChange(); + /// true if the joint may have a redo applied, otherwise false. + bool canPerformRedo() const { return mUndoneJointStatesIndex > 0; } /// - /// Redoes the last rotation set, if any. + /// Gets the 'public' scale of the joint. /// - void redoLastRotationChange(); + LLVector3 getPublicScale() const { return mCurrentState.mScale; } /// - /// Gets whether a redo of this joints rotation may be performed. + /// Sets the 'public' scale of the joint. /// - /// true if the joint can have a redo applied, otherwise false. - bool canRedoRotation() const { return mUndoneRotationIndex > 0; } - - /// - /// Gets the scale the animator wishes the joint to have. - /// - LLVector3 getScaleDelta() const { return mScaleDelta; } - - /// - /// Sets the scale the animator wishes the joint to have. - /// - void setScaleDelta(const LLVector3& scale); - - /// - /// Undoes the last scale set, if any. - /// - void undoLastScaleChange(); - - /// - /// Redoes the last scale set, if any. - /// - void redoLastScaleChange(); + void setPublicScale(const LLVector3& scale); /// /// Exchanges the rotations between two joints. @@ -168,52 +150,69 @@ class FSJointPose /// void revertJoint(); - LLVector3 getTargetPosition() const { return mPositionDelta + mBeginningPosition; } - LLQuaternion getTargetRotation() const { return mRotation.getTargetRotation(); } - LLVector3 getTargetScale() const { return mScaleDelta + mBeginningScale; } + LLQuaternion getTargetRotation() const { return mCurrentState.getTargetRotation(); } + LLVector3 getTargetPosition() const { return mCurrentState.getTargetPosition(); } + LLVector3 getTargetScale() const { return mCurrentState.getTargetScale(); } /// /// Gets the pointer to the jointstate for the joint this represents. /// LLPointer getJointState() const { return mJointState; } - /// - /// A class wrapping base and delta rotation, attempting to keep baseRotation as secret as possible. - /// Among other things, facilitates easy undo/redo through the joint-recapture process. - /// - class FSJointRotation + class FSJointState { public: - FSJointRotation(LLQuaternion base) { baseRotation.set(base); } - - FSJointRotation(LLQuaternion base, LLQuaternion delta) + FSJointState(LLJoint* joint) { - baseRotation.set(base); - deltaRotation.set(delta); + mBaseRotation.set(joint->getRotation()); + mBasePosition.set(joint->getPosition()); + mBaseScale.set(joint->getScale()); } - FSJointRotation() = default; + FSJointState() = default; - LLQuaternion baseRotation; - LLQuaternion deltaRotation; - LLQuaternion getTargetRotation() const { return deltaRotation * baseRotation; } + LLQuaternion getTargetRotation() const { return mRotation * mBaseRotation; } + LLVector3 getTargetPosition() const { return mPosition + mBasePosition; } + LLVector3 getTargetScale() const { return mScale + mBaseScale; } void reflectRotation() { - baseRotation.mQ[VX] *= -1; - baseRotation.mQ[VZ] *= -1; - deltaRotation.mQ[VX] *= -1; - deltaRotation.mQ[VZ] *= -1; + mBaseRotation.mQ[VX] *= -1; + mBaseRotation.mQ[VZ] *= -1; + mRotation.mQ[VX] *= -1; + mRotation.mQ[VZ] *= -1; } - void set(const FSJointRotation& jRot) + void cloneRotationFrom(FSJointState otherState) { - baseRotation.set(jRot.baseRotation); - deltaRotation.set(jRot.deltaRotation); + mBaseRotation.set(otherState.mBaseRotation); + mRotation.set(otherState.mRotation); } + + private: + FSJointState(FSJointState* state) + { + mBaseRotation.set(state->mBaseRotation); + mBasePosition.set(state->mBasePosition); + mBaseScale.set(state->mBaseScale); + + mRotation.set(state->mRotation); + mPosition.set(state->mPosition); + mScale.set(state->mScale); + } + + public: + LLQuaternion mRotation; + LLVector3 mPosition; + LLVector3 mScale; + + private: + LLQuaternion mBaseRotation; + LLVector3 mBasePosition; + LLVector3 mBaseScale; }; -private: + private: std::string mJointName = ""; // expected to be a match to LLJoint.getName() for a joint implementation. LLPointer mJointState{ nullptr }; @@ -223,32 +222,16 @@ private: /// bool mIsCollisionVolume{ false }; - FSJointRotation mRotation; - std::deque mLastSetRotationDeltas; - size_t mUndoneRotationIndex = 0; - std::chrono::system_clock::time_point mTimeLastUpdatedRotation = std::chrono::system_clock::now(); + std::deque mLastSetJointStates; + size_t mUndoneJointStatesIndex = 0; + std::chrono::system_clock::time_point mTimeLastUpdatedCurrentState = std::chrono::system_clock::now(); - LLVector3 mPositionDelta; - LLVector3 mBeginningPosition; - std::deque mLastSetPositionDeltas; - size_t mUndonePositionIndex = 0; - std::chrono::system_clock::time_point mTimeLastUpdatedPosition = std::chrono::system_clock::now(); + FSJointState mCurrentState; + FSJointState mBeginningState; - /// - /// Joint scales require special treatment, as they do not revert when we stop animating an avatar. - /// - LLVector3 mScaleDelta; - LLVector3 mBeginningScale; - std::deque mLastSetScaleDeltas; - size_t mUndoneScaleIndex = 0; - std::chrono::system_clock::time_point mTimeLastUpdatedScale = std::chrono::system_clock::now(); - - template - void addToUndo(T delta, size_t* undoIndex, std::deque* dequeue, std::chrono::system_clock::time_point* timeLastUpdated); - - template T undoLastChange(T thingToSet, size_t* undoIndex, std::deque* dequeue); - - template T redoLastChange(T thingToSet, size_t* undoIndex, std::deque* dequeue); + void addStateToUndo(FSJointState stateToAddToUndo); + FSJointState undoLastStateChange(FSJointState currentState); + FSJointState redoLastStateChange(FSJointState currentState); }; #endif // FS_JOINTPPOSE_H diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index 6845e8419c..2aabb98985 100644 --- a/indra/newview/fsposeranimator.cpp +++ b/indra/newview/fsposeranimator.cpp @@ -78,7 +78,7 @@ void FSPoserAnimator::setPosingAvatarJoint(LLVOAvatar* avatar, const FSPoserJoin posingMotion->removeJointFromState(jointPose); } -void FSPoserAnimator::resetAvatarJoint(LLVOAvatar* avatar, const FSPoserJoint& joint) +void FSPoserAnimator::undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) { if (!isAvatarSafeToUse(avatar)) return; @@ -94,27 +94,7 @@ void FSPoserAnimator::resetAvatarJoint(LLVOAvatar* avatar, const FSPoserJoint& j if (!jointPose) return; - jointPose->setPositionDelta(LLVector3()); - jointPose->setRotationDelta(LLQuaternion()); -} - -void FSPoserAnimator::undoLastJointRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) -{ - if (!isAvatarSafeToUse(avatar)) - return; - - FSPosingMotion* posingMotion = getPosingMotion(avatar); - if (!posingMotion) - return; - - if (posingMotion->isStopped()) - return; - - FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); - if (!jointPose) - return; - - jointPose->undoLastRotationChange(); + jointPose->undoLastChange(); if (style == NONE || style == DELTAMODE) return; @@ -123,10 +103,10 @@ void FSPoserAnimator::undoLastJointRotation(LLVOAvatar* avatar, const FSPoserJoi if (!oppositeJointPose) return; - oppositeJointPose->undoLastRotationChange(); + oppositeJointPose->undoLastChange(); } -void FSPoserAnimator::undoLastJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) +void FSPoserAnimator::resetJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) { if (!isAvatarSafeToUse(avatar)) return; @@ -142,7 +122,9 @@ void FSPoserAnimator::undoLastJointPosition(LLVOAvatar* avatar, const FSPoserJoi if (!jointPose) return; - jointPose->undoLastPositionChange(); + jointPose->setPublicRotation(LLQuaternion()); + jointPose->setPublicPosition(LLVector3()); + jointPose->setPublicScale(LLVector3()); if (style == NONE || style == DELTAMODE) return; @@ -151,94 +133,12 @@ void FSPoserAnimator::undoLastJointPosition(LLVOAvatar* avatar, const FSPoserJoi if (!oppositeJointPose) return; - oppositeJointPose->undoLastPositionChange(); + oppositeJointPose->setPublicRotation(LLQuaternion()); + oppositeJointPose->setPublicPosition(LLVector3()); + oppositeJointPose->setPublicScale(LLVector3()); } -void FSPoserAnimator::undoLastJointScale(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) -{ - if (!isAvatarSafeToUse(avatar)) - return; - - FSPosingMotion* posingMotion = getPosingMotion(avatar); - if (!posingMotion) - return; - - if (posingMotion->isStopped()) - return; - - FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); - if (!jointPose) - return; - - jointPose->undoLastScaleChange(); - - if (style == NONE || style == DELTAMODE) - return; - - FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint.mirrorJointName()); - if (!oppositeJointPose) - return; - - oppositeJointPose->undoLastScaleChange(); -} - -void FSPoserAnimator::resetJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) -{ - if (!isAvatarSafeToUse(avatar)) - return; - - FSPosingMotion* posingMotion = getPosingMotion(avatar); - if (!posingMotion) - return; - - if (posingMotion->isStopped()) - return; - - FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); - if (!jointPose) - return; - - jointPose->setPositionDelta(LLVector3()); - - if (style == NONE || style == DELTAMODE) - return; - - FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint.mirrorJointName()); - if (!oppositeJointPose) - return; - - oppositeJointPose->setPositionDelta(LLVector3()); -} - -void FSPoserAnimator::resetJointScale(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) -{ - if (!isAvatarSafeToUse(avatar)) - return; - - FSPosingMotion* posingMotion = getPosingMotion(avatar); - if (!posingMotion) - return; - - if (posingMotion->isStopped()) - return; - - FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); - if (!jointPose) - return; - - jointPose->setScaleDelta(LLVector3()); - - if (style == NONE || style == DELTAMODE) - return; - - FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint.mirrorJointName()); - if (!oppositeJointPose) - return; - - oppositeJointPose->setScaleDelta(LLVector3()); -} - -bool FSPoserAnimator::canRedoJointRotation(LLVOAvatar* avatar, const FSPoserJoint& joint) +bool FSPoserAnimator::canRedoJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint) { if (!isAvatarSafeToUse(avatar)) return false; @@ -254,10 +154,10 @@ bool FSPoserAnimator::canRedoJointRotation(LLVOAvatar* avatar, const FSPoserJoin if (!jointPose) return false; - return jointPose->canRedoRotation(); + return jointPose->canPerformRedo(); } -void FSPoserAnimator::redoLastJointRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) +void FSPoserAnimator::redoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) { if (!isAvatarSafeToUse(avatar)) return; @@ -273,7 +173,7 @@ void FSPoserAnimator::redoLastJointRotation(LLVOAvatar* avatar, const FSPoserJoi if (!jointPose) return; - jointPose->redoLastRotationChange(); + jointPose->redoLastChange(); if (style == NONE || style == DELTAMODE) return; @@ -282,63 +182,7 @@ void FSPoserAnimator::redoLastJointRotation(LLVOAvatar* avatar, const FSPoserJoi if (!oppositeJointPose) return; - oppositeJointPose->redoLastRotationChange(); -} - -void FSPoserAnimator::redoLastJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) -{ - if (!isAvatarSafeToUse(avatar)) - return; - - FSPosingMotion* posingMotion = getPosingMotion(avatar); - if (!posingMotion) - return; - - if (posingMotion->isStopped()) - return; - - FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); - if (!jointPose) - return; - - jointPose->redoLastPositionChange(); - - if (style == NONE || style == DELTAMODE) - return; - - FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint.mirrorJointName()); - if (!oppositeJointPose) - return; - - oppositeJointPose->redoLastPositionChange(); -} - -void FSPoserAnimator::redoLastJointScale(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style) -{ - if (!isAvatarSafeToUse(avatar)) - return; - - FSPosingMotion* posingMotion = getPosingMotion(avatar); - if (!posingMotion) - return; - - if (posingMotion->isStopped()) - return; - - FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); - if (!jointPose) - return; - - jointPose->redoLastScaleChange(); - - if (style == NONE || style == DELTAMODE) - return; - - FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint.mirrorJointName()); - if (!oppositeJointPose) - return; - - oppositeJointPose->redoLastScaleChange(); + oppositeJointPose->redoLastChange(); } LLVector3 FSPoserAnimator::getJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint) const @@ -355,7 +199,7 @@ LLVector3 FSPoserAnimator::getJointPosition(LLVOAvatar* avatar, const FSPoserJoi if (!jointPose) return pos; - return jointPose->getPositionDelta(); + return jointPose->getPublicPosition(); } void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& position, E_BoneDeflectionStyles style) @@ -377,7 +221,7 @@ void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* j if (!jointPose) return; - LLVector3 positionDelta = jointPose->getPositionDelta() - position; + LLVector3 positionDelta = jointPose->getPublicPosition() - position; switch (style) { @@ -385,13 +229,13 @@ void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* j case MIRROR_DELTA: case SYMPATHETIC_DELTA: case SYMPATHETIC: - jointPose->setPositionDelta(position); + jointPose->setPublicPosition(position); break; case DELTAMODE: case NONE: default: - jointPose->setPositionDelta(position); + jointPose->setPublicPosition(position); return; } @@ -399,18 +243,18 @@ void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* j if (!oppositeJointPose) return; - LLVector3 oppositeJointPosition = oppositeJointPose->getPositionDelta(); + LLVector3 oppositeJointPosition = oppositeJointPose->getPublicPosition(); switch (style) { case MIRROR: case MIRROR_DELTA: - oppositeJointPose->setPositionDelta(oppositeJointPosition + positionDelta); + oppositeJointPose->setPublicPosition(oppositeJointPosition + positionDelta); break; case SYMPATHETIC_DELTA: case SYMPATHETIC: - oppositeJointPose->setPositionDelta(oppositeJointPosition - positionDelta); + oppositeJointPose->setPublicPosition(oppositeJointPosition - positionDelta); break; default: @@ -493,7 +337,7 @@ LLVector3 FSPoserAnimator::getJointRotation(LLVOAvatar* avatar, const FSPoserJoi if (!jointPose) return vec3; - return translateRotationFromQuaternion(translation, negation, jointPose->getRotationDelta()); + return translateRotationFromQuaternion(translation, negation, jointPose->getPublicRotation()); } void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& absRotation, @@ -523,27 +367,27 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j case SYMPATHETIC: case MIRROR: if (rotationStyle == DELTAIC_ROT) - jointPose->setRotationDelta(deltaRot * jointPose->getRotationDelta()); + jointPose->setPublicRotation(deltaRot * jointPose->getPublicRotation()); else - jointPose->setRotationDelta(absRot); + jointPose->setPublicRotation(absRot); break; case SYMPATHETIC_DELTA: case MIRROR_DELTA: - jointPose->setRotationDelta(deltaRot * jointPose->getRotationDelta()); + jointPose->setPublicRotation(deltaRot * jointPose->getPublicRotation()); break; case DELTAMODE: - jointPose->setRotationDelta(deltaRot * jointPose->getRotationDelta()); + jointPose->setPublicRotation(deltaRot * jointPose->getPublicRotation()); return; case NONE: default: if (rotationStyle == DELTAIC_ROT) - jointPose->setRotationDelta(deltaRot * jointPose->getRotationDelta()); + jointPose->setPublicRotation(deltaRot * jointPose->getPublicRotation()); else - jointPose->setRotationDelta(absRot); + jointPose->setPublicRotation(absRot); return; } @@ -560,7 +404,7 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j break; case SYMPATHETIC_DELTA: - oppositeJointPose->setRotationDelta(deltaRot * oppositeJointPose->getRotationDelta()); + oppositeJointPose->setPublicRotation(deltaRot * oppositeJointPose->getPublicRotation()); break; case MIRROR: @@ -569,7 +413,7 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j case MIRROR_DELTA: inv_quat = LLQuaternion(-deltaRot.mQ[VX], deltaRot.mQ[VY], -deltaRot.mQ[VZ], deltaRot.mQ[VW]); - oppositeJointPose->setRotationDelta(inv_quat * oppositeJointPose->getRotationDelta()); + oppositeJointPose->setPublicRotation(inv_quat * oppositeJointPose->getPublicRotation()); break; default: @@ -603,6 +447,45 @@ void FSPoserAnimator::reflectJoint(LLVOAvatar* avatar, const FSPoserJoint* joint } } +void FSPoserAnimator::symmetrizeLeftToRightOrRightToLeft(LLVOAvatar* avatar, bool rightToLeft) +{ + if (!isAvatarSafeToUse(avatar)) + return; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return; + + for (size_t index = 0; index != PoserJoints.size(); ++index) + { + if (!PoserJoints[index].dontFlipOnMirror()) + continue; + + bool currentlyPosing = isPosingAvatarJoint(avatar, PoserJoints[index]); + if (!currentlyPosing) + continue; + + auto oppositeJoint = getPoserJointByName(PoserJoints[index].mirrorJointName()); + if (!oppositeJoint) + continue; + + bool currentlyPosingOppositeJoint = isPosingAvatarJoint(avatar, *oppositeJoint); + if (!currentlyPosingOppositeJoint) + continue; + + FSJointPose* rightJointPose = posingMotion->getJointPoseByJointName(PoserJoints[index].jointName()); + FSJointPose* leftJointPose = posingMotion->getJointPoseByJointName(oppositeJoint->jointName()); + + if (!leftJointPose || !rightJointPose) + return; + + if (rightToLeft) + leftJointPose->mirrorRotationFrom(rightJointPose); + else + rightJointPose->mirrorRotationFrom(leftJointPose); + } +} + void FSPoserAnimator::flipEntirePose(LLVOAvatar* avatar) { if (!isAvatarSafeToUse(avatar)) @@ -750,7 +633,7 @@ LLVector3 FSPoserAnimator::getJointScale(LLVOAvatar* avatar, const FSPoserJoint& if (!jointPose) return scale; - return jointPose->getScaleDelta(); + return jointPose->getPublicScale(); } void FSPoserAnimator::setJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& scale, E_BoneDeflectionStyles style) @@ -772,7 +655,7 @@ void FSPoserAnimator::setJointScale(LLVOAvatar* avatar, const FSPoserJoint* join if (!jointPose) return; - jointPose->setScaleDelta(scale); + jointPose->setPublicScale(scale); FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint->mirrorJointName()); if (!oppositeJointPose) return; @@ -783,7 +666,7 @@ void FSPoserAnimator::setJointScale(LLVOAvatar* avatar, const FSPoserJoint* join case MIRROR: case SYMPATHETIC_DELTA: case MIRROR_DELTA: - oppositeJointPose->setScaleDelta(scale); + oppositeJointPose->setPublicScale(scale); break; case DELTAMODE: @@ -810,10 +693,10 @@ bool FSPoserAnimator::tryGetJointSaveVectors(LLVOAvatar* avatar, const FSPoserJo if (!jointPose) return false; - LLQuaternion rotationDelta = jointPose->getRotationDelta(); + LLQuaternion rotationDelta = jointPose->getPublicRotation(); rotationDelta.getEulerAngles(&rot->mV[VX], &rot->mV[VY], &rot->mV[VZ]); - pos->set(jointPose->getPositionDelta()); - scale->set(jointPose->getScaleDelta()); + pos->set(jointPose->getPublicPosition()); + scale->set(jointPose->getPublicScale()); *baseRotationIsZero = jointPose->isBaseRotationZero(); return true; @@ -836,7 +719,7 @@ void FSPoserAnimator::loadJointRotation(LLVOAvatar* avatar, const FSPoserJoint* jointPose->zeroBaseRotation(); LLQuaternion rot = translateRotationToQuaternion(SWAP_NOTHING, NEGATE_NOTHING, rotation); - jointPose->setRotationDelta(rot); + jointPose->setPublicRotation(rot); } void FSPoserAnimator::loadJointPosition(LLVOAvatar* avatar, const FSPoserJoint* joint, bool loadPositionAsDelta, LLVector3 position) @@ -853,9 +736,9 @@ void FSPoserAnimator::loadJointPosition(LLVOAvatar* avatar, const FSPoserJoint* return; if (loadPositionAsDelta) - jointPose->setPositionDelta(position); + jointPose->setPublicPosition(position); else - jointPose->setPositionDelta(position); + jointPose->setPublicPosition(position); } void FSPoserAnimator::loadJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, bool loadScaleAsDelta, LLVector3 scale) @@ -872,9 +755,9 @@ void FSPoserAnimator::loadJointScale(LLVOAvatar* avatar, const FSPoserJoint* joi return; if (loadScaleAsDelta) - jointPose->setScaleDelta(scale); + jointPose->setPublicScale(scale); else - jointPose->setScaleDelta(scale); + jointPose->setPublicScale(scale); } const FSPoserAnimator::FSPoserJoint* FSPoserAnimator::getPoserJointByName(const std::string& jointName) diff --git a/indra/newview/fsposeranimator.h b/indra/newview/fsposeranimator.h index 7f7ce6e334..cf6652a62f 100644 --- a/indra/newview/fsposeranimator.h +++ b/indra/newview/fsposeranimator.h @@ -398,46 +398,19 @@ public: void setPosingAvatarJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, bool shouldPose); /// - /// Resets the supplied PoserJoint to its position/rotation/scale it was when poser was started. - /// - /// The avatar having the joint to which we refer. - /// The joint to be reset. - void resetAvatarJoint(LLVOAvatar* avatar, const FSPoserJoint& joint); - - /// - /// Undoes the last applied rotation to the supplied PoserJoint. - /// - /// The avatar having the joint to which we refer. - /// The joint with the rotation to undo. - void undoLastJointRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); - - /// - /// Undoes the last applied position to the supplied PoserJoint. - /// - /// The avatar having the joint to which we refer. - /// The joint with the position to undo. - void undoLastJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); - - /// - /// Undoes the last applied scale to the supplied PoserJoint. - /// - /// The avatar having the joint to which we refer. - /// The joint with the scale to undo. - void undoLastJointScale(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); - - /// - /// Resets the position of the supplied PoserJoint. + /// Resets the supplied PoserJoint to the position it had when poser was started. /// /// The avatar having the joint to which we refer. /// The joint with the position to reset. - void resetJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); + /// The style to apply the reset with; if a style that support more than one joint, more that one joint will be reset. + void resetJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); /// - /// Resets the scale of the supplied joint to initial values. + /// Undoes the last applied change (rotation, position or scale) to the supplied PoserJoint. /// /// The avatar having the joint to which we refer. - /// The joint with the scale to reset. - void resetJointScale(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); + /// The joint with the rotation to undo. + void undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); /// /// Determines if a redo action is currently permitted for the supplied joint. @@ -445,28 +418,14 @@ public: /// The avatar having the joint to which we refer. /// The joint to query. /// True if a redo action is available, otherwise false. - bool canRedoJointRotation(LLVOAvatar* avatar, const FSPoserJoint& joint); + bool canRedoJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint); /// - /// Re-does the last undone rotation to the supplied PoserJoint. + /// Re-does the last undone change (rotation, position or scale) to the supplied PoserJoint. /// /// The avatar having the joint to which we refer. /// The joint with the rotation to redo. - void redoLastJointRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); - - /// - /// Re-does the last undone position to the supplied PoserJoint. - /// - /// The avatar having the joint to which we refer. - /// The joint with the position to redo. - void redoLastJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); - - /// - /// Re-does the last undone scale to the supplied PoserJoint. - /// - /// The avatar having the joint to which we refer. - /// The joint with the scale to redo. - void redoLastJointScale(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); + void redoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style); /// /// Gets the position of a joint for the supplied avatar. @@ -541,6 +500,13 @@ public: /// The avatar whose pose should flip left-right. void flipEntirePose(LLVOAvatar* avatar); + /// + /// Symmetrizes the rotations of the joints from one side of the supplied avatar to the other. + /// + /// The avatar whose joints to symmetrizet. + /// 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. /// diff --git a/indra/newview/fsposingmotion.cpp b/indra/newview/fsposingmotion.cpp index fab282552c..fc418dd911 100644 --- a/indra/newview/fsposingmotion.cpp +++ b/indra/newview/fsposingmotion.cpp @@ -260,7 +260,7 @@ void FSPosingMotion::setAllRotationsToZero() for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) { poserJoint_iter->zeroBaseRotation(); - poserJoint_iter->setRotationDelta(LLQuaternion::DEFAULT); + poserJoint_iter->setPublicRotation(LLQuaternion::DEFAULT); } } 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 3ec5684a08..83a0ccdd81 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -1,11 +1,12 @@ +width="430"> Inv_BodyShape Inv_Object @@ -283,7 +284,7 @@ width="403"> name="poser_stack" orientation="vertical" top="0" - width="607"> + width="700"> + height="310" + width="700"> function="Poser.CommitSpinner" parameter="3"/> + - + - Rotation: - - + name="modifier_tabs" + tab_height="20" + tab_width="90" + tab_group="1" + tab_position="top" + top_pad="0" + width="215"> + + + + Up/Down: + + + Left/Right: + + + Roll: + + + + + + + + + + + + + + Position X: + + + + + + + + + Position Y: + + + + + + + + + Position Z: + + + + + + + + + Scale X: + + + + + + + + + Scale Y: + + + + + + + + + Scale Z: + + + + + + + + + + follows="left|top|bottom" + height="44" + background_visible="false" + layout="topleft" + mouse_opaque="false" + left="0" + top_pad="2" + name="trackball_button_panel" + width="232"> + + - - Up/Down: - - - - - - - Left/Right: - - - - - - - Roll: - - - - - background_visible="false" layout="topleft" mouse_opaque="false" - left_pad="2" + left_pad="-26" visible="false" name="poses_loadSave" top="0" width="225"> left="0" name="button_controls_panel" width="800"> - left_pad="0" top_delta="0" name="advbutton_spacer_panel" - width="36"/> + width="56" + top_pad="-1" + left="2"/> @@ -1390,7 +1652,7 @@ width="403"> name="load_poses_button" left_pad="1" top_delta="0" - width="97"/> + width="95"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - From 5e0a207fb349f0c77df56e796c7f25947179b0c6 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Mon, 10 Mar 2025 22:57:46 +0000 Subject: [PATCH 2/9] Poser: Correct base rotation zeroing Fix load/save diff/pose button text --- indra/newview/fsfloaterposer.cpp | 5 ++++- indra/newview/fsjointpose.cpp | 13 +++---------- indra/newview/fsjointpose.h | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 1812a85bdb..15d09cf485 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -704,7 +704,7 @@ void FSFloaterPoser::onPoseMenuAction(const LLSD& param) loadPoseFromXml(avatar, poseName, loadType); onJointTabSelect(); refreshJointScrollListMembers(); - setSavePosesButtonText(mPoserAnimator.allBaseRotationsAreZero(avatar)); + setSavePosesButtonText(!mPoserAnimator.allBaseRotationsAreZero(avatar)); } bool FSFloaterPoser::notDoubleClicked() @@ -893,6 +893,9 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose version = (S32)control_map["value"].asInteger(); } + if (startFromZeroRot) + mPoserAnimator.setAllAvatarStartingRotationsToZero(avatar); + bool loadPositionsAndScalesAsDeltas = false; if (version > 3) loadPositionsAndScalesAsDeltas = true; diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index 1693331b41..9a863b9b7e 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -49,7 +49,6 @@ FSJointPose::FSJointPose(LLJoint* joint, U32 usage, bool isCollisionVolume) mIsCollisionVolume = isCollisionVolume; mCurrentState = FSJointState(joint); - mBeginningState = FSJointState(joint); } void FSJointPose::setPublicPosition(const LLVector3& pos) @@ -178,13 +177,7 @@ void FSJointPose::mirrorRotationFrom(FSJointPose* fromJoint) void FSJointPose::revertJoint() { - LLJoint* joint = mJointState->getJoint(); - if (!joint) - return; - - joint->setRotation(mBeginningState.getTargetRotation()); - joint->setPosition(mBeginningState.getTargetPosition()); - joint->setScale(mBeginningState.getTargetScale()); + mCurrentState.revertJointToBase(mJointState->getJoint()); } void FSJointPose::reflectRotation() @@ -200,7 +193,7 @@ void FSJointPose::zeroBaseRotation() if (mIsCollisionVolume) return; - mBeginningState.mRotation = LLQuaternion::DEFAULT; + mCurrentState.zeroBaseRotation(); } bool FSJointPose::isBaseRotationZero() const @@ -208,5 +201,5 @@ bool FSJointPose::isBaseRotationZero() const if (mIsCollisionVolume) return true; - return mBeginningState.mRotation == LLQuaternion::DEFAULT; + return mCurrentState.baseRotationIsZero(); } diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index b521458efe..850d3b539a 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -189,6 +189,20 @@ class FSJointPose mRotation.set(otherState.mRotation); } + bool baseRotationIsZero() const { return mBaseRotation == LLQuaternion::DEFAULT; } + + void zeroBaseRotation() { mBaseRotation = LLQuaternion::DEFAULT; } + + void revertJointToBase(LLJoint* joint) const + { + if (!joint) + return; + + joint->setRotation(mBaseRotation); + joint->setPosition(mBasePosition); + joint->setScale(mBaseScale); + } + private: FSJointState(FSJointState* state) { @@ -227,7 +241,6 @@ class FSJointPose std::chrono::system_clock::time_point mTimeLastUpdatedCurrentState = std::chrono::system_clock::now(); FSJointState mCurrentState; - FSJointState mBeginningState; void addStateToUndo(FSJointState stateToAddToUndo); FSJointState undoLastStateChange(FSJointState currentState); From 63579a9da7d2cf31404d3697062d4002e463116a Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Tue, 11 Mar 2025 16:29:07 +0000 Subject: [PATCH 3/9] Poser: Update save version and only set to teePose if reading less buggy XML --- indra/newview/fsfloaterposer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 15d09cf485..a9a8328783 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -390,7 +390,7 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi { bool savingDiff = !mPoserAnimator.allBaseRotationsAreZero(avatar); LLSD record; - record["version"]["value"] = (S32)5; + record["version"]["value"] = (S32)6; record["startFromTeePose"]["value"] = !savingDiff; LLVector3 rotation, position, scale, zeroVector; @@ -893,7 +893,7 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose version = (S32)control_map["value"].asInteger(); } - if (startFromZeroRot) + if (version > 5 && startFromZeroRot) mPoserAnimator.setAllAvatarStartingRotationsToZero(avatar); bool loadPositionsAndScalesAsDeltas = false; From 6a12b00b82184dbe15449005e9b43d883fbcd7e5 Mon Sep 17 00:00:00 2001 From: Beq Date: Fri, 21 Feb 2025 16:49:39 +0000 Subject: [PATCH 4/9] Visual posing v1.0 - rotation and major joint selection in world Working visualiser, shows "correct" naturla rotation, toggled in poser for world rot fully working manipulator --- indra/newview/CMakeLists.txt | 2 + indra/newview/app_settings/settings.xml | 11 + indra/newview/fsfloaterposer.cpp | 189 +++- indra/newview/fsfloaterposer.h | 16 +- indra/newview/fsjointpose.cpp | 12 + indra/newview/fsjointpose.h | 14 +- indra/newview/fsjointrotatetool.cpp | 66 ++ indra/newview/fsjointrotatetool.h | 32 + indra/newview/fsmaniprotatejoint.cpp | 1371 +++++++++++++++++++++++ indra/newview/fsmaniprotatejoint.h | 121 ++ indra/newview/fsposeranimator.cpp | 16 + indra/newview/fsposeranimator.h | 2 + indra/newview/llmaniprotate.h | 5 +- indra/newview/lltoolcomp.cpp | 162 +++ indra/newview/lltoolcomp.h | 39 + indra/newview/lltoolmgr.cpp | 4 + indra/newview/lltoolmgr.h | 1 + indra/newview/llviewerwindow.cpp | 7 + indra/newview/llvoavatar.cpp | 221 ++++ indra/newview/llvoavatar.h | 3 + 20 files changed, 2259 insertions(+), 35 deletions(-) create mode 100644 indra/newview/fsjointrotatetool.cpp create mode 100644 indra/newview/fsjointrotatetool.h create mode 100644 indra/newview/fsmaniprotatejoint.cpp create mode 100644 indra/newview/fsmaniprotatejoint.h diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index a4ab4a66a7..9aae2a38c6 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -146,6 +146,7 @@ set(viewer_SOURCE_FILES fslslbridgerequest.cpp fslslpreproc.cpp fslslpreprocviewer.cpp + fsmaniprotatejoint.cpp fsmoneytracker.cpp fsnamelistavatarmenu.cpp fsnearbychatbarlistener.cpp @@ -960,6 +961,7 @@ set(viewer_HEADER_FILES fslslpreproc.h fslslpreprocviewer.h fsmoneytracker.h + fsmaniprotatejoint.h fsnamelistavatarmenu.h fsnearbychatbarlistener.h fsnearbychatcontrol.h diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 118f209adb..45d2941acf 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -26198,5 +26198,16 @@ Change of this parameter will affect the layout of buttons in notification toast Value 1.0 + FSManipRotateJointUseNaturalDirection + + Comment + use the natural bone direction instead of world rotation + Persist + 1 + Type + Boolean + Value + 1 + diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index a9a8328783..48a4144eb8 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -42,6 +42,7 @@ #include "llwindow.h" #include "llvoavatarself.h" #include "llinventoryfunctions.h" +#include "lltoolcomp.h" namespace { @@ -115,7 +116,7 @@ bool FSFloaterPoser::postBuild() { onJointTabSelect(); setRotationChangeButtons(false, false); - }); + }); mAvatarSelectionScrollList = getChild("avatarSelection_scroll"); mAvatarSelectionScrollList->setCommitOnSelectionChange(true); @@ -215,6 +216,20 @@ bool FSFloaterPoser::postBuild() mScaleYSpnr = getChild("adv_scaley_spinner"); mScaleZSpnr = getChild("adv_scalez_spinner"); + mBtnJointRotate = getChild("button_joint_rotate_tool"); + + mCommitCallbackRegistrar.add("Poser.SetRotateTool", + [this](LLUICtrl*, const LLSD&) + { + LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); + LLToolMgr::getInstance()->getCurrentToolset()->selectTool( (LLTool *) FSToolCompPose::getInstance()); + } + ); + + LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); + LLToolMgr::getInstance()->getCurrentToolset()->selectTool( (LLTool *) FSToolCompPose::getInstance()); + FSToolCompPose::getInstance()->setAvatar( gAgentAvatarp); + return true; } @@ -226,7 +241,13 @@ void FSFloaterPoser::onOpen(const LLSD& key) onJointTabSelect(); refreshPoseScroll(mHandPresetsScrollList, POSE_PRESETS_HANDS_SUBDIRECTORY); startPosingSelf(); - + if (LLToolMgr::getInstance()->getCurrentToolset() != gCameraToolset) + { + mLastToolset = LLToolMgr::getInstance()->getCurrentToolset(); + } + LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); + LLToolMgr::getInstance()->getCurrentToolset()->selectTool(FSToolCompPose::getInstance()); + FSToolCompPose::getInstance()->setAvatar( gAgentAvatarp); LLFloater::onOpen(key); } @@ -235,6 +256,11 @@ void FSFloaterPoser::onClose(bool app_quitting) if (gSavedSettings.getBOOL(POSER_STOPPOSINGWHENCLOSED_SAVE_KEY)) stopPosingAllAvatars(); + if (mLastToolset) + { + LLToolMgr::getInstance()->setCurrentToolset(mLastToolset); + } + FSToolCompPose::getInstance()->setAvatar(nullptr); LLFloater::onClose(app_quitting); } @@ -552,6 +578,33 @@ void FSFloaterPoser::onClickRecaptureSelectedBones() refreshTrackpadCursor(); refreshTextHighlightingOnJointScrollLists(); } +void FSFloaterPoser::updatePosedBones() +{ + auto selectedJoints = getUiSelectedPoserJoints(); + if (selectedJoints.size() < 1) + return; + + LLVOAvatar *avatar = getUiSelectedAvatar(); + if (!avatar) + return; + + if (!mPoserAnimator.isPosingAvatar(avatar)) + return; + + for (auto item : selectedJoints) + { + bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); + if (!currentlyPosing) + continue; + + mPoserAnimator.recaptureJointAsDelta(avatar, *item, getJointTranslation(item->jointName()), getJointNegation(item->jointName())); + } + + setSavePosesButtonText(true); + refreshRotationSlidersAndSpinners(); + refreshTrackpadCursor(); + refreshTextHighlightingOnJointScrollLists(); +} void FSFloaterPoser::onClickBrowsePoseCache() { @@ -1331,6 +1384,93 @@ void FSFloaterPoser::enableOrDisableRedoButton() mRedoChangeBtn->setEnabled(shouldEnableRedoButton); } +void FSFloaterPoser::selectJointByName(const std::string& jointName) +{ + LLTabContainer* tabContainer = mJointsTabs; + std::vector panels = { + mPositionRotationPnl, + mBodyJointsPnl, + mFaceJointsPnl, + mHandsTabs, + mMiscJointsPnl, + mCollisionVolumesPnl + }; + + std::vector scrollLists = { + mEntireAvJointScroll, + mBodyJointsScrollList, + mFaceJointsScrollList, + mHandJointsScrollList, + mMiscJointsScrollList, + mCollisionVolumesScrollList + }; + + bool found = false; + for (S32 i = 0; i < tabContainer->getTabCount(); ++i) + { + LLPanel* panel = tabContainer->getPanelByIndex(i); + tabContainer->selectTabPanel(panel); + + // Special handling for Hands tab + if (panel == mHandsTabs) + { + mHandsTabs->selectTabPanel(mHandsJointsPnl); + } + + for (auto scrollList : scrollLists) + { + scrollList->deselectAllItems(); + } + + auto scrollList = getScrollListForTab(panel); + + std::vector items = scrollList->getAllData(); + for (auto item : items) + { + auto* userData = static_cast(item->getUserdata()); + if (userData && userData->jointName() == jointName) + { + tabContainer->selectTab(i); + scrollList->selectNthItem(scrollList->getItemIndex(item)); + scrollList->scrollToShowSelected(); + getUiSelectedPoserJoints(); + return; // Exit the loop once we've found and selected the joint + } + } + } + LL_WARNS() << "Joint not found: " << jointName << LL_ENDL; +} + +LLScrollListCtrl* FSFloaterPoser::getScrollListForTab(LLPanel * tabPanel) const +{ + if (tabPanel == mPositionRotationPnl) + { + return mEntireAvJointScroll; + } + else if (tabPanel == mBodyJointsPnl) + { + return mBodyJointsScrollList; + } + else if (tabPanel == mFaceJointsPnl) + { + return mFaceJointsScrollList; + } + else if (tabPanel == mHandsTabs) + { + return mHandJointsScrollList; + } + else if (tabPanel == mMiscJointsPnl) + { + return mMiscJointsScrollList; + } + else if (tabPanel == mCollisionVolumesPnl) + { + return mCollisionVolumesScrollList; + } + + LL_WARNS() << "Unknown tab panel: " << tabPanel << LL_ENDL; + return nullptr; +} std::vector FSFloaterPoser::getUiSelectedPoserJoints() const { std::vector joints; @@ -1342,42 +1482,20 @@ std::vector FSFloaterPoser::getUiSelectedPoserJo } LLScrollListCtrl* scrollList{ nullptr }; - + + scrollList = getScrollListForTab(activeTab); if (activeTab == mPositionRotationPnl) { mEntireAvJointScroll->selectFirstItem(); - scrollList = mEntireAvJointScroll; } - else if (activeTab == mBodyJointsPnl) - { - scrollList = mBodyJointsScrollList; - } - else if (activeTab == mFaceJointsPnl) - { - scrollList = mFaceJointsScrollList; - } - else if (activeTab == mHandsTabs) + else if (activeTab == mHandsTabs ) { auto activeHandsSubTab = mHandsTabs->getCurrentPanel(); if (!activeHandsSubTab) { return joints; } - - if (activeHandsSubTab == mHandsJointsPnl) - { - scrollList = mHandJointsScrollList; - } } - else if (activeTab == mMiscJointsPnl) - { - scrollList = mMiscJointsScrollList; - } - else if (activeTab == mCollisionVolumesPnl) - { - scrollList = mCollisionVolumesScrollList; - } - if (!scrollList) { return joints; @@ -1391,7 +1509,18 @@ 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 ); + } + } return joints; } @@ -1907,6 +2036,10 @@ S32 FSFloaterPoser::getJointNegation(const std::string& jointName) const void FSFloaterPoser::onAvatarSelect() { LLVOAvatar* avatar = getUiSelectedAvatar(); + if(avatar) + { + FSToolCompPose::getInstance()->setAvatar(avatar); + } mStartStopPosingBtn->setEnabled(couldAnimateAvatar(avatar)); bool arePosingSelected = mPoserAnimator.isPosingAvatar(avatar); diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index bd17406e7f..447dfbe5ec 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -29,6 +29,7 @@ #define FS_FLOATER_POSER_H #include "llfloater.h" +#include "lltoolmgr.h" #include "fsposeranimator.h" class FSVirtualTrackpad; @@ -77,7 +78,9 @@ class FSFloaterPoser : public LLFloater { friend class LLFloaterReg; FSFloaterPoser(const LLSD &key); - +public: + void updatePosedBones(); + void selectJointByName(const std::string& jointName); private: bool postBuild() override; void onOpen(const LLSD& key) override; @@ -176,7 +179,7 @@ class FSFloaterPoser : public LLFloater /// The avatar UUID to find on the avatars scroll list. /// The scroll-list index for the supplied avatar, if found, otherwise -1. S32 getAvatarListIndexForUuid(const LLUUID& toFind) const; - + /// /// There are several control-callbacks manipulating rotations etc, they all devolve to these. /// In these are the appeals to the posing business layer. @@ -185,11 +188,10 @@ class FSFloaterPoser : public LLFloater /// Using a set, then a get does not guarantee the value you just set. /// There may be +/- PI difference two axes, because harmonics. /// Thus keep your UI synced with less gets. - /// + /// void setSelectedJointsRotation(const LLVector3& absoluteRot, const LLVector3& deltaRot); void setSelectedJointsPosition(F32 x, F32 y, F32 z); void setSelectedJointsScale(F32 x, F32 y, F32 z); - /// /// Yeilds the rotation of the first selected joint (one may multi-select). /// @@ -202,6 +204,7 @@ class FSFloaterPoser : public LLFloater LLVector3 getPositionOfFirstSelectedJoint() const; LLVector3 getScaleOfFirstSelectedJoint() const; + LLScrollListCtrl* getScrollListForTab(LLPanel * tabPanel) const; // Pose load/save void createUserPoseDirectoryIfNeeded(); void onToggleLoadSavePanel(); @@ -435,7 +438,11 @@ class FSFloaterPoser : public LLFloater /// static F32 unWrapScale(F32 scale); + LLToolset* mLastToolset{ nullptr }; + LLTool* mJointRotTool{ nullptr }; + LLVector3 mLastSliderRotation; + FSVirtualTrackpad* mAvatarTrackball{ nullptr }; LLSliderCtrl* mTrackpadSensitivitySlider{ nullptr }; @@ -476,6 +483,7 @@ class FSFloaterPoser : public LLFloater LLButton* mToggleDeltaModeBtn{ nullptr }; LLButton* mRedoChangeBtn{ nullptr }; LLButton* mSetToTposeButton{ nullptr }; + LLButton* mBtnJointRotate{ nullptr }; LLLineEditor* mPoseSaveNameEditor{ nullptr }; diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index 9a863b9b7e..d30d0eb68c 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -144,6 +144,18 @@ void FSJointPose::recaptureJoint() addStateToUndo(FSJointState(mCurrentState)); mCurrentState = FSJointState(joint); } +void FSJointPose::recaptureJointAsDelta() +{ + if (mIsCollisionVolume) + return; + + LLJoint* joint = mJointState->getJoint(); + if (!joint) + return; + + addToUndo(mRotation, &mUndoneRotationIndex, &mLastSetRotationDeltas, &mTimeLastUpdatedRotation); + mRotation.updateRotation(joint->getRotation()); +} void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) { diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index 850d3b539a..fe2d200bd1 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -143,6 +143,11 @@ 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(); + /// /// Reverts the position/rotation/scale to their values when the animation begun. @@ -170,10 +175,17 @@ 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() { diff --git a/indra/newview/fsjointrotatetool.cpp b/indra/newview/fsjointrotatetool.cpp new file mode 100644 index 0000000000..c235ceeeb0 --- /dev/null +++ b/indra/newview/fsjointrotatetool.cpp @@ -0,0 +1,66 @@ +#include "fsjointrotatetool.h" +#include "llviewerobject.h" +#include "llvoavatar.h" +#include "llviewercontrol.h" + +FSJointRotateTool::FSJointRotateTool() : LLTool(std::string("Rotate Joint")) +{ +} + +void FSJointRotateTool::handleSelect() +{ + LL_INFOS() << "Rotate tool selected" << LL_ENDL; +} + +void FSJointRotateTool::handleDeselect() +{ + LL_INFOS() << "Rotate tool deselected" << LL_ENDL; +} + +bool FSJointRotateTool::handleMouseDown(S32 x, S32 y, MASK mask) +{ + if (findSelectedManipulator(x, y)) + { + LLVOAvatar* avatar = gAgentAvatarp; // Assume the avatar is the agent avatar + if (avatar) + { + rotateJoint(avatar); + } + } + return true; +} + +bool FSJointRotateTool::handleHover(S32 x, S32 y, MASK mask) +{ + return findSelectedManipulator(x, y); +} + +void FSJointRotateTool::render() +{ + computeManipulatorSize(); + renderManipulators(); +} + +void FSJointRotateTool::rotateJoint(LLVOAvatar* avatar) +{ + // Use joint rotation APIs, perhaps from FSPoserAnimator + LL_INFOS() << "Rotating joint" << LL_ENDL; +} + +bool FSJointRotateTool::findSelectedManipulator(S32 x, S32 y) +{ + // Implement logic to detect user selection of a manipulator + return false; +} + +void FSJointRotateTool::computeManipulatorSize() +{ + mManipulatorSize = 5.0f; // Example size +} + +void FSJointRotateTool::renderManipulators() +{ + // Render manipulators (axes or rotation handles) using OpenGL + gGL.color4f(0.5f, 0.5f, 0.5f, 0.5f); // Example color + gGL.flush(); +} diff --git a/indra/newview/fsjointrotatetool.h b/indra/newview/fsjointrotatetool.h new file mode 100644 index 0000000000..728c946b02 --- /dev/null +++ b/indra/newview/fsjointrotatetool.h @@ -0,0 +1,32 @@ +#ifndef FS_JOINTROTATETOOL_H +#define FS_JOINTROTATETOOL_H + +#include "lltool.h" +#include "llbbox.h" +#include "llvoavatar.h" + +class FSJointRotateTool : public LLTool, public LLSingleton +{ + LLSINGLETON(FSJointRotateTool); + +public: + void handleSelect() override; + void handleDeselect() override; + bool handleMouseDown(S32 x, S32 y, MASK mask) override; + bool handleHover(S32 x, S32 y, MASK mask) override; + void render() override; + +private: + void rotateJoint(LLVOAvatar* avatar); + bool findSelectedManipulator(S32 x, S32 y); + void computeManipulatorSize(); + void renderManipulators(); + + LLBBox mBBox; + F32 mManipulatorSize; + S32 mHighlightedAxis; + F32 mHighlightedDirection; + bool mForce; +}; + +#endif // FS_JOINTROTATETOOL_H diff --git a/indra/newview/fsmaniprotatejoint.cpp b/indra/newview/fsmaniprotatejoint.cpp new file mode 100644 index 0000000000..255d0f369d --- /dev/null +++ b/indra/newview/fsmaniprotatejoint.cpp @@ -0,0 +1,1371 @@ +/** + * @file fsmaniproatejoint.cpp + * @brief custom manipulator for rotating joints + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Phoenix Firestorm Viewer Source Code + * Copyright (c) 2025 Beq Janus @ Second Life + * + * 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 "fsmaniprotatejoint.h" +#include "llrendersphere.h" +#include "llvoavatar.h" +#include "lljoint.h" +#include "llagent.h" // for gAgent, etc. +#include "llagentcamera.h" +#include "llcontrol.h" +#include "llresmgr.h" // for LLLocale +#include "llviewerwindow.h" +#include "llviewercamera.h" +#include "llviewershadermgr.h" +#include "llfloaterreg.h" +#include "fsfloaterposer.h" +// ------------------------------------- + +extern U64MicrosecondsImplicit gFrameTime; +extern LLControlGroup gSavedSettings; + + +/** + * @brief Renders a pulsing sphere at a specified joint position in the world. + * + * This function creates a visual indicator in the form of a pulsing sphere + * at a given joint position. The sphere's size oscillates over time to create + * a pulsing effect, making it easier to identify in the 3D space. + * + * @param joint_world_position The position of the joint in world coordinates where the sphere should be rendered. + * @param color The color of the sphere. Defaults to white (1.0, 1.0, 1.0, 1.0). + * + * @return void + * + */ +void renderPulsingSphere(const LLVector3& joint_world_position, const LLColor4& color=LLColor4(1.f, 1.f, 1.f, 1.f)) +{ + constexpr float MAX_SPHERE_RADIUS = 0.05f; // Base radius in agent-space units. + constexpr float PULSE_AMPLITUDE = 0.01f; // Additional radius variation. + constexpr float PULSE_FREQUENCY = 1.f; // Pulses per second. + constexpr float PULSE_TIME_DOMAIN = 5.f; // Keep the time input small. + + // Get the current time (in seconds) from the global timer. + const U64 timeMicrosec = gFrameTime; + // Convert microseconds to seconds + const F64 timeSec = std::fmod( static_cast(timeMicrosec) / 1000000.0, PULSE_TIME_DOMAIN ); + // Compute the pulse factor using a sine wave. This value oscillates between 0 and 1. + float pulseFactor = 0.75f + 0.25f * std::sin(PULSE_FREQUENCY * 2.f * F_PI * static_cast(timeSec)); + + // Calculate the current sphere radius. + float currentRadius = MAX_SPHERE_RADIUS - PULSE_AMPLITUDE * pulseFactor; + + LLGLSUIDefault gls_ui; + gGL.getTexUnit(0)->bind(LLViewerFetchedTexture::sWhiteImagep); + LLGLDepthTest gls_depth(GL_TRUE); + LLGLEnable gl_blend(GL_BLEND); + + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.pushMatrix(); + { + + // Translate to the joint's position + gGL.translatef(joint_world_position.mV[VX], joint_world_position.mV[VY], joint_world_position.mV[VZ]); + gGL.pushMatrix(); + { + gDebugProgram.bind(); + + LLGLEnable cull_face(GL_CULL_FACE); + LLGLDepthTest gls_depth(GL_FALSE); + gGL.pushMatrix(); + { + LLColor4 color; + gGL.color4f(0.f, 0.f, 1.f, 0.3f); + gGL.diffuseColor4f(0.f, 0.f, 1.f, 0.3f); + + gGL.scalef(currentRadius, currentRadius, currentRadius); + + gSphere.render(); + gGL.flush(); + } + gGL.popMatrix(); + + gUIProgram.bind(); + } + gGL.popMatrix(); + } + gGL.popMatrix(); + + // Check for OpenGL errors + GLenum err; + while ((err = glGetError()) != GL_NO_ERROR) + { + LL_INFOS() << "OpenGL Error: " << err << LL_ENDL; + } +} + + +bool isMouseOverJoint(S32 mouseX, S32 mouseY, const LLVector3& jointWorldPos, F32 jointRadius, F32& outDistanceFromCamera) +{ + LLViewerCamera* camera = LLViewerCamera::getInstance(); + + // Transform joint world position to screen coordinates + LLCoordGL jointScreenPos; + camera->projectPosAgentToScreen(jointWorldPos, jointScreenPos); + + // Get the world view rect + LLRect world_view_rect = gViewerWindow->getWorldViewRectScaled(); + F32 half_width = (F32)world_view_rect.getWidth() / 2.f; + F32 half_height = (F32)world_view_rect.getHeight() / 2.f; + + // Convert mouse coordinates to be relative to the center of the screen + LLVector2 mousePos((F32)mouseX - half_width, (F32)mouseY - half_height); + + // Convert joint screen position to be relative to the center of the screen + LLVector2 joint2d(jointScreenPos.mX - half_width, jointScreenPos.mY - half_height); + + // Calculate the distance between mouse and joint in screen space + LLVector2 delta = joint2d - mousePos; + + // Calculate the distance from the camera to the joint + outDistanceFromCamera = (jointWorldPos - camera->getOrigin()).magVec(); + + // Calculate the apparent radius of the joint on the screen + F32 apparentRadius = jointRadius * camera->getPixelMeterRatio() / outDistanceFromCamera; + + // Check if the mouse is within the joint's radius + return (delta.magVecSquared() < apparentRadius * apparentRadius); +} + +//static +std::unordered_map FSManipRotateJoint::sReferenceUpVectors = {}; + +//static +const std::vector FSManipRotateJoint::sSelectableJoints = +{ + // head, torso, legs + { "mHead" }, + { "mNeck" }, + { "mPelvis" }, + { "mChest" }, + { "mTorso" }, + { "mCollarLeft" }, + { "mShoulderLeft" }, + { "mElbowLeft" }, + { "mWristLeft" }, + { "mCollarRight" }, + { "mShoulderRight" }, + { "mElbowRight" }, + { "mWristRight" }, + { "mHipLeft" }, + { "mKneeLeft" }, + { "mAnkleLeft" }, + { "mHipRight" }, + { "mKneeRight" }, + { "mAnkleRight" }, + +}; + +const std::unordered_map FSManipRotateJoint::sRingParams = +{ + { LL_ROT_Z, { LL_ROT_Z, LLVector4(1.f, 1.f, SELECTED_MANIPULATOR_SCALE, 1.f), 0.f, LLVector3(), LLColor4(0.f,0.f,1.f,1.f), LLColor4(0.f,0.f,1.f,0.3f), 2 } }, + { LL_ROT_Y, { LL_ROT_Y, LLVector4(1.f, SELECTED_MANIPULATOR_SCALE, 1.f, 1.f), 90.f, LLVector3(1.f,0.f,0.f), LLColor4(0.f,1.f,0.f,1.f), LLColor4(0.f,1.f,0.f,0.3f), 1 } }, + { LL_ROT_X, { LL_ROT_X, LLVector4(SELECTED_MANIPULATOR_SCALE, 1.f, 1.f, 1.f), 90.f, LLVector3(0.f,1.f,0.f), LLColor4(1.f,0.f,0.f,1.f), LLColor4(1.f,0.f,0.f,0.3f), 0 } } +}; +// Helper function: Builds an alignment quaternion from the computed bone axes. +// This quaternion rotates from the default coordinate system (assumed to be +// X = (1,0,0), Y = (0,1,0), Z = (0,0,1)) into the bone’s natural coordinate system. +LLQuaternion FSManipRotateJoint::computeAlignmentQuat( const BoneAxes& boneAxes ) const +{ + LLQuaternion alignmentQuat(boneAxes.naturalX, boneAxes.naturalY, boneAxes.naturalZ); + alignmentQuat.normalize(); + return alignmentQuat; +} + +/** + * @brief Computes the natural axes for the bone associated with the joint. + * + * This function calculates a set of orthogonal axes that represent the natural + * orientation of the bone. It uses the joint's end point and a reference vector + * to determine these axes, with the Z-axis along the joint/bone and the Y axis + * perpendicular and in the plan of the world vertical, except when the joint is vertical + * in which case the X-axis is used. You can provide a custom reference vector by setting + * an entry in the sReferenceUpVectors map (joints like thumbs need this ideally). + * + * @return BoneAxes A struct containing the computed natural X, Y, and Z axes for the bone. + */ +FSManipRotateJoint::BoneAxes FSManipRotateJoint::computeBoneAxes() const +{ + BoneAxes axes; + + // Use 0,0,0 as local start for joint. + LLVector3 joint_local_pos (0.f,0.f,0.f); + + // Transform the local endpoint (mEnd) into world space. + LLVector3 localEnd = mJoint->getEnd(); + + axes.naturalZ = localEnd - joint_local_pos; + axes.naturalZ.normalize(); + + // Choose a reference vector. We'll use world up (0,1,0) as the default, + // but check for an override. + LLVector3 reference(0.f, 0.f, 1.f); + std::string jointName = mJoint->getName(); + auto iter = sReferenceUpVectors.find(jointName); + if (iter != sReferenceUpVectors.end()) + { + reference = iter->second; + } + + // However, if the bone is nearly vertical relative to world up, then world up may be almost co-linear with naturalZ. + if (std::fabs(axes.naturalZ * reference) > 0.99f) + { + // Use an alternate reference (+x) + reference = LLVector3(1.f, 0.f, 0.f); + } + + // Now, we want the naturalY to be the projection of the chosen reference onto the plane + // between natrualZ and rference. + // naturalY = reference - (naturalZ dot reference)*naturalZ (I think) + axes.naturalY = reference - (axes.naturalZ * (axes.naturalZ * reference)); + axes.naturalY.normalize(); + + // Compute naturalX as the cross product of naturalY and naturalZ. + axes.naturalX = axes.naturalY % axes.naturalZ; + axes.naturalX.normalize(); + + return axes; +} + +/** + * @brief Highlights the joint sphere that the mouse is hovering over. + * + * This function iterates through all "selectable joints" of the avatar and checks if the mouse + * is hovering over any of them. It updates the highlighted joint to be the closest one to the camera + * that the mouse is over. + * the Selectable Joints are currently statically defined as a usable subset of all joints. + * TODO(Beq) allow other subsets to be highlighted/selected when editing specifica areas such as face or hands. + * + * @param mouseX The x-coordinate of the mouse cursor in screen space. + * @param mouseY The y-coordinate of the mouse cursor in screen space. + * + * @return void + * + * @note This function updates the mHighlightedJoint and mHighlightedPartDistance member variables. + */ +void FSManipRotateJoint::highlightHoverSpheres(S32 mouseX, S32 mouseY) +{ + // Ensure we have an avatar to work with. + if (!mAvatar) return; + mHighlightedJoint = nullptr; // reset the highlighted joint + + // Iterate through the avatar's joint map. + for (const auto& entry : getSelectableJoints()) + { + LLJoint* joint = mAvatar->getJoint(std::string(entry)); + if (!joint) continue; + + // Update the joint's world matrix to ensure its position is current. + joint->updateWorldMatrixParent(); + joint->updateWorldMatrix(); + + // Retrieve the joint's world position (in agent space). + LLVector3 jointWorldPos = joint->getWorldPosition(); + LLCachedControl target_radius(gSavedSettings, "FSManipRotateJointTargetSize", 0.2f); + F32 distance_from_camera; + if (isMouseOverJoint(mouseX, mouseY, jointWorldPos, target_radius, distance_from_camera) == true) + { + // we want to highlight the closest + if (!mHighlightedJoint || mHighlightedPartDistance > distance_from_camera) + { + mHighlightedJoint = joint; + mHighlightedPartDistance = distance_from_camera; + } + } + } +} + +FSManipRotateJoint::FSManipRotateJoint(LLToolComposite* composite) +: LLManipRotate(composite) +{} + +// ------------------------------------- + +void FSManipRotateJoint::setJoint(LLJoint* joint) +{ + mJoint = joint; + + // Save initial rotation as baseline for delta rotation + if (mJoint) + { + mSavedJointRot = mJoint->getWorldRotation(); + mBoneAxes = computeBoneAxes(); + mNaturalAlignmentQuat = computeAlignmentQuat(mBoneAxes); + } +} + +void FSManipRotateJoint::setAvatar(LLVOAvatar* avatar) +{ + mAvatar = avatar; +} + +/** + * @brief Handles the selection of the joint rotate tool + * + * This function is called when the rotate tool is selectedfor manipulation. + * + * @note It serves no purpose right now but might be useful once we add transform and scale + */ +void FSManipRotateJoint::handleSelect() +{ + // Not entirely sure this is needed in the current implementation. + if (mJoint) + { + mSavedJointRot = mJoint->getWorldRotation(); + } +} + +// We override this because we don't have a selection center from LLSelectMgr. +// Mostly copied from the base class, but without the selection center logic. +// Instead, we get the joint's position, convert to global, and store in mRotationCenter. +/** + * @brief Updates the visibility and positioning of the joint manipulator. + * + * This function calculates whether the joint manipulator should be visible + * and updates its position and scale based on the camera view and UI scale. + * It also determines if the camera is edge-on to the manipulator's axis. + * The "edge on" state is not used currently but is used in the base class for stepping + * + * @return bool Returns true if the manipulator is visible, false otherwise. + */ +bool FSManipRotateJoint::updateVisiblity() +{ + if (!mJoint) + { + // No joint to manipulate, not visible + return false; + } + + if (!hasMouseCapture()) + { + mRotationCenter = gAgent.getPosGlobalFromAgent( mJoint->getWorldPosition() ); + } + + bool visible = false; + + //Assume that UI scale factor is equivalent for X and Y axis + F32 ui_scale_factor = LLUI::getScaleFactor().mV[VX]; + + const LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal( mRotationCenter ); // Convert from world/agent to global + + const auto * viewer_camera = LLViewerCamera::getInstance(); + visible = viewer_camera->projectPosAgentToScreen(agent_space_center, mCenterScreen ); + if( visible ) + { + mCenterToCam = gAgentCamera.getCameraPositionAgent() - agent_space_center; + mCenterToCamNorm = mCenterToCam; + mCenterToCamMag = mCenterToCamNorm.normalize(); + LLVector3 cameraAtAxis = viewer_camera->getAtAxis(); + cameraAtAxis.normalize(); + + F32 z_dist = -1.f * (mCenterToCam * cameraAtAxis); + + // Don't drag manip if object too far away + if (mCenterToCamMag > 0.001f) + { + F32 fraction_of_fov = RADIUS_PIXELS / static_cast(viewer_camera->getViewHeightInPixels()); + F32 apparent_angle = fraction_of_fov * viewer_camera->getView(); // radians + mRadiusMeters = z_dist * tan(apparent_angle); + mRadiusMeters *= ui_scale_factor; + + mCenterToProfilePlaneMag = mRadiusMeters * mRadiusMeters / mCenterToCamMag; + mCenterToProfilePlane = -mCenterToProfilePlaneMag * mCenterToCamNorm; + } + else + { + visible = false; + } + } + + mCamEdgeOn = false; + F32 axis_onto_cam = mManipPart >= LL_ROT_X ? llabs( getConstraintAxis() * mCenterToCamNorm ) : 0.f; + if( axis_onto_cam < AXIS_ONTO_CAM_TOLERANCE ) + { + mCamEdgeOn = true; + } + + return visible; +} + + +/** + * @brief Updates the scale of a specific manipulator part. + * + * This function smoothly interpolates the scale of a given manipulator part + * towards its target scale. It uses a predefined set of ring parameters and + * applies smooth interpolation for visual consistency. + * + * @param part The manipulator part to update (e.g., LL_ROT_X, LL_ROT_Y, LL_ROT_Z). + * @param scales Reference to the LLVector4 containing current scales, which will be updated. + * + * @note This function modifies the 'scales' parameter in-place. + */ +void FSManipRotateJoint::updateManipulatorScale(EManipPart part, LLVector4& scales) +{ + auto iter = sRingParams.find(part); + if (iter != sRingParams.end()) + { + scales = lerp(scales, iter->second.targetScale, LLSmoothInterpolation::getInterpolant(MANIPULATOR_SCALE_HALF_LIFE)); + } +} + +// Render a single ring using the given parameters and pass index (for multi-pass rendering). +void FSManipRotateJoint::renderRingPass(const RingRenderParams& params, float radius, float width, int pass) +{ + gGL.pushMatrix(); + { + // If an extra rotation is specified, apply it. + if (params.extraRotateAngle != 0.f) + { + gGL.rotatef(params.extraRotateAngle, params.extraRotateAxis.mV[0], + params.extraRotateAxis.mV[1], params.extraRotateAxis.mV[2]); + } + // Get the appropriate scale value from mManipulatorScales. + float scaleVal = 1.f; + switch (params.scaleIndex) + { + case 0: scaleVal = mManipulatorScales.mV[VX]; break; + case 1: scaleVal = mManipulatorScales.mV[VY]; break; + case 2: scaleVal = mManipulatorScales.mV[VZ]; break; + case 3: scaleVal = mManipulatorScales.mV[VW]; break; + default: break; + } + gGL.scalef(scaleVal, scaleVal, scaleVal); + gl_ring(radius, width, params.primaryColor, params.secondaryColor, CIRCLE_STEPS, pass); + } + gGL.popMatrix(); +} + +/** + * @brief Renders the manipulator rings for joint rotation. + * + * This function renders the manipulator rings used for rotating joints. It handles + * the rendering of the center sphere and individual rings based on the current + * manipulation state and highlighted parts. + * + * @param agent_space_center The center point of the manipulator in agent space. + * @param rotation The rotation to be applied to the manipulator rings. + * + * @return void + */ +void FSManipRotateJoint::renderManipulatorRings(const LLVector3& agent_space_center, const LLQuaternion& rotation) +{ + F32 width_meters = WIDTH_PIXELS * mRadiusMeters / RADIUS_PIXELS; + // Translate to the joint's position + const auto joint_world_position = mJoint->getWorldPosition(); + gDebugProgram.bind(); + gGL.pushMatrix(); + { + LLGLEnable cull_face(GL_CULL_FACE); + LLGLDepthTest gls_depth(GL_FALSE); + LLGLEnable clip_plane0(GL_CLIP_PLANE0); + gGL.translatef(joint_world_position.mV[VX], joint_world_position.mV[VY], joint_world_position.mV[VZ]); + + LLMatrix4 rot_mat(rotation); + gGL.multMatrix((GLfloat*)rot_mat.mMatrix); + + for ( int pass = 0; pass < 2; ++pass) + { + if( mManipPart == LL_NO_PART || mManipPart == LL_ROT_ROLL || mHighlightedPart == LL_ROT_ROLL) + { + renderCenterSphere( mRadiusMeters); + for (auto & ring_params : sRingParams) + { + const auto part = ring_params.first; + const auto& params = ring_params.second; + if (mHighlightedPart == part) + { + updateManipulatorScale(part, mManipulatorScales); + } + renderRingPass(params, mRadiusMeters, width_meters, pass); + } + if( mManipPart == LL_ROT_ROLL || mHighlightedPart == LL_ROT_ROLL) + { + static const auto roll_color = LLColor4(1.f,0.65f,0.0f,0.4f); + updateManipulatorScale(mManipPart, mManipulatorScales); + gGL.pushMatrix(); + { + // Cancel the rotation applied earlier: + LLMatrix4 inv_rot_mat(rotation); + inv_rot_mat.invert(); + gGL.multMatrix((GLfloat*)inv_rot_mat.mMatrix); + renderCenterCircle( mRadiusMeters*1.2f, roll_color, roll_color ); + } + gGL.popMatrix(); + } + } + else + { + auto iter = sRingParams.find(mManipPart); + if (iter != sRingParams.end()) + { + updateManipulatorScale(mManipPart, mManipulatorScales); + renderRingPass(iter->second, mRadiusMeters, width_meters, pass); + } + } + } + } + gGL.popMatrix(); + gUIProgram.bind(); +} + +void FSManipRotateJoint::renderCenterCircle(const F32 radius, const LLColor4 normal_color, const LLColor4 highlight_color) +{ + gGL.pushMatrix(); + { + LLGLEnable cull_face(GL_CULL_FACE); + LLGLDepthTest gls_depth(GL_FALSE); + + const int segments = 64; + glLineWidth(6.0f); // Set the desired line thickness + + // Compute a scale factor that already factors in the radius. + float scale = radius; + scale *= (mManipulatorScales.mV[VX] + mManipulatorScales.mV[VY] + + mManipulatorScales.mV[VZ] + mManipulatorScales.mV[VW]) / 4.0f; + gGL.diffuseColor4fv(normal_color.mV); + gGL.scalef(scale, scale, scale); + + // Rotate the unit circle so its normal (0,0,1) aligns with mCenterToCamNorm. + LLVector3 defaultNormal(0.f, 0.f, 1.f); + LLVector3 targetNormal = mCenterToCamNorm; + targetNormal.normalize(); // Ensure it is normalized. + + LLVector3 rotationAxis = defaultNormal % targetNormal; // Cross product. + F32 dot = defaultNormal * targetNormal; // Dot product. + F32 angle = acosf(dot) * RAD_TO_DEG; // Convert to degrees. + + if (rotationAxis.magVec() > 0.001f) + { + gGL.rotatef(angle, rotationAxis.mV[0], rotationAxis.mV[1], rotationAxis.mV[2]); + } + + // Draw a unit circle in the XY plane (which is now rotated correctly). + gGL.begin(LLRender::LINE_LOOP); + for (int i = 0; i < segments; i++) + { + float theta = 2.0f * 3.14159f * i / segments; + // Use a unit circle here. + LLVector3 offset(cosf(theta), sinf(theta), 0.f); + gGL.vertex3fv(offset.mV); + } + gGL.end(); + + glLineWidth(1.0f); // Reset the line width. + } + gGL.popMatrix(); +} + +void FSManipRotateJoint::renderCenterSphere(const F32 radius, const LLColor4 normal_color, const LLColor4 highlight_color) +{ + gGL.pushMatrix(); + { + LLGLEnable cull_face(GL_CULL_FACE); + LLGLDepthTest gls_depth(GL_FALSE); + + float scale = radius*0.8f; + + if (mManipPart == LL_ROT_GENERAL || mHighlightedPart == LL_ROT_GENERAL) + { + mManipulatorScales = lerp(mManipulatorScales, LLVector4(1.f, 1.f, 1.f, SELECTED_MANIPULATOR_SCALE), LLSmoothInterpolation::getInterpolant(MANIPULATOR_SCALE_HALF_LIFE)); + gGL.diffuseColor4fv(highlight_color.mV); + scale *= mManipulatorScales.mV[VW]; + } + else + { + // no part selected, just a semi transp white sphere + gGL.diffuseColor4fv(normal_color.mV); + // Use an average of the manipulator scales when no specific part is selected + scale *= (mManipulatorScales.mV[VX] + mManipulatorScales.mV[VY] + mManipulatorScales.mV[VZ] + mManipulatorScales.mV[VW]) / 4.0f; + } + + gGL.scalef(scale, scale, scale); + gSphere.render(); + + gGL.flush(); + } + gGL.popMatrix(); +} + + +/** + * @brief Renders the joint rotation manipulator and associated visual elements. + * + * This function is responsible for rendering the joint rotation manipulator, + * including the manipulator rings, axes, and debug information. It handles + * the visibility checks, GL state setup, and calls to specific rendering + * functions for different components of the manipulator. + * + * The function performs the following main tasks: + * 1. Checks for the presence of a valid joint and avatar. + * 2. Updates the visibility and rotation center. + * 3. Sets up the GL state for rendering. + * 4. Renders a pulsing sphere for highlighted joints (if applicable). + * 5. Updates joint world matrices. + * 6. Computes the active rotation based on user settings. + * 7. Renders the manipulator axes and rings. + * 8. Displays debug information (Euler angles, including delta of the active drag). + * + * This function does not take any parameters and does not return a value. + * It operates on the internal state of the FSManipRotateJoint object. + */ +void FSManipRotateJoint::render() +{ + // Early-out if no joint or avatar. + if (!mJoint || !mAvatar) + { + return; + } + + // update visibility and rotation center. + if (!updateVisiblity()) + { + return; + } + // Setup GL state. + LLGLSUIDefault gls_ui; + gGL.getTexUnit(0)->bind(LLViewerFetchedTexture::sWhiteImagep); + LLGLDepthTest gls_depth(GL_TRUE); + LLGLEnable gl_blend(GL_BLEND); + + // Optionally, if another joint is highlighted, render a pulsing sphere. + if (mHighlightedJoint && mJoint != mHighlightedJoint) + { + mHighlightedJoint->updateWorldMatrixParent(); + mHighlightedJoint->updateWorldMatrix(); + renderPulsingSphere(mHighlightedJoint->getWorldPosition()); + } + + // Update joint world matrices. + mJoint->updateWorldMatrixParent(); + mJoint->updateWorldMatrix(); + + const LLQuaternion joint_world_rotation = mJoint->getWorldRotation(); + + const LLQuaternion parentWorldRot = (mJoint->getParent()) ? mJoint->getParent()->getWorldRotation() : LLQuaternion::DEFAULT; + + LLQuaternion currentLocalRot = mJoint->getRotation(); + + LLQuaternion rotatedNaturalAlignment = mNaturalAlignmentQuat * currentLocalRot; + rotatedNaturalAlignment.normalize(); + // Compute the final world alignment: + LLQuaternion final_world_alignment = rotatedNaturalAlignment * parentWorldRot; + final_world_alignment.normalize(); + + const LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + + + + LLCachedControl use_natural_direction(gSavedSettings, "FSManipRotateJointUseNaturalDirection", true); + LLQuaternion active_rotation = use_natural_direction? final_world_alignment : joint_world_rotation; + // Render the manipulator rings in a separate function. + gGL.matrixMode(LLRender::MM_MODELVIEW); + renderAxes(agent_space_center, mRadiusMeters * 1.5f, active_rotation); + renderManipulatorRings(agent_space_center, active_rotation); + + // Debug: render joint's Euler angles for diagnostic purposes. + LLVector3 euler_angles; + active_rotation.getEulerAngles(&euler_angles.mV[0], + &euler_angles.mV[1], + &euler_angles.mV[2]); + euler_angles *= RAD_TO_DEG; + euler_angles.mV[0] = ll_round(fmodf(euler_angles.mV[0] + 360.f, 360.f), 0.05f); + euler_angles.mV[1] = ll_round(fmodf(euler_angles.mV[1] + 360.f, 360.f), 0.05f); + euler_angles.mV[2] = ll_round(fmodf(euler_angles.mV[2] + 360.f, 360.f), 0.05f); + renderNameXYZ(euler_angles); +} + +void FSManipRotateJoint::renderAxes(const LLVector3& agent_space_center, F32 size, const LLQuaternion& rotation) +{ + LLGLEnable cull_face(GL_CULL_FACE); + LLGLEnable clip_plane0(GL_CLIP_PLANE0); + LLGLDepthTest gls_depth(GL_FALSE); + gGL.pushMatrix(); + gGL.translatef(agent_space_center.mV[VX], agent_space_center.mV[VY], agent_space_center.mV[VZ]); + + LLMatrix4 rot_mat(rotation); + + gGL.multMatrix((GLfloat*)rot_mat.mMatrix); + + gGL.begin(LLRender::LINES); + + // X-axis (Red) + gGL.color4f(1.0f, 0.0f, 0.0f, 1.0f); + gGL.vertex3f(-size, 0.0f, 0.0f); + gGL.vertex3f(size, 0.0f, 0.0f); + + // Y-axis (Green) + gGL.color4f(0.0f, 1.0f, 0.0f, 1.0f); + gGL.vertex3f(0.0f, -size, 0.0f); + gGL.vertex3f(0.0f, size, 0.0f); + + // Z-axis (Blue) + gGL.color4f(0.0f, 0.0f, 1.0f, 1.0f); + gGL.vertex3f(0.0f, 0.0f, -size); + gGL.vertex3f(0.0f, 0.0f, size); + + gGL.end(); + + gGL.popMatrix(); +} + +//static +std::string FSManipRotateJoint::getManipPartString(EManipPart part) +{ + switch (part) + { + case LL_NO_PART: return "None"; + case LL_X_ARROW: return "X Arrow"; + case LL_Y_ARROW: return "Y Arrow"; + case LL_Z_ARROW: return "Z Arrow"; + case LL_YZ_PLANE: return "YZ Plane"; + case LL_XZ_PLANE: return "XZ Plane"; + case LL_XY_PLANE: return "XY Plane"; + case LL_CORNER_NNN: return "Corner ---"; + case LL_CORNER_NNP: return "Corner --+"; + case LL_CORNER_NPN: return "Corner -+-"; + case LL_CORNER_NPP: return "Corner -++"; + case LL_CORNER_PNN: return "Corner +--"; + case LL_CORNER_PNP: return "Corner +-+"; + case LL_CORNER_PPN: return "Corner ++-"; + case LL_CORNER_PPP: return "Corner +++"; + case LL_FACE_POSZ: return "Face +Z"; + case LL_FACE_POSX: return "Face +X"; + case LL_FACE_POSY: return "Face +Y"; + case LL_FACE_NEGX: return "Face -X"; + case LL_FACE_NEGY: return "Face -Y"; + case LL_FACE_NEGZ: return "Face -Z"; + case LL_EDGE_NEGX_NEGY: return "Edge -X-Y"; + case LL_EDGE_NEGX_POSY: return "Edge -X+Y"; + case LL_EDGE_POSX_NEGY: return "Edge +X-Y"; + case LL_EDGE_POSX_POSY: return "Edge +X+Y"; + case LL_EDGE_NEGY_NEGZ: return "Edge -Y-Z"; + case LL_EDGE_NEGY_POSZ: return "Edge -Y+Z"; + case LL_EDGE_POSY_NEGZ: return "Edge +Y-Z"; + case LL_EDGE_POSY_POSZ: return "Edge +Y+Z"; + case LL_EDGE_NEGZ_NEGX: return "Edge -Z-X"; + case LL_EDGE_NEGZ_POSX: return "Edge -Z+X"; + case LL_EDGE_POSZ_NEGX: return "Edge +Z-X"; + case LL_EDGE_POSZ_POSX: return "Edge +Z+X"; + case LL_ROT_GENERAL: return "Rotate General"; + case LL_ROT_X: return "Rotate X"; + case LL_ROT_Y: return "Rotate Y"; + case LL_ROT_Z: return "Rotate Z"; + case LL_ROT_ROLL: return "Rotate Roll"; + default: return "Unknown"; + } +} + +/** + * @brief Renders the XYZ coordinates and additional information as text overlay on the screen. + * + * This function displays the X, Y, and Z coordinates of the given vector, along with the delta angle, + * joint name, and manipulation part. It creates a semi-transparent background and renders the text + * with shadow effects for better visibility. + * + * @param vec The LLVector3 containing the X, Y, and Z coordinates to be displayed. + * + * @return void + * + * @note This function assumes the existence of class member variables such as mLastAngle, mJoint, and mManipPart. + * It also uses global functions and objects like gViewerWindow, LLUI, and LLFontGL. + */ +void FSManipRotateJoint::renderNameXYZ(const LLVector3 &vec) +{ + const S32 PAD = 10; + S32 window_center_x = gViewerWindow->getWorldViewRectScaled().getWidth() / 2; + S32 window_center_y = gViewerWindow->getWorldViewRectScaled().getHeight() / 2; + S32 vertical_offset = window_center_y - VERTICAL_OFFSET; + + gGL.pushMatrix(); + { + LLUIImagePtr imagep = LLUI::getUIImage("Rounded_Square"); + gViewerWindow->setup2DRender(); + const LLVector2& display_scale = gViewerWindow->getDisplayScale(); + gGL.color4f(0.f, 0.f, 0.f, 0.7f); + + imagep->draw( + (S32)((window_center_x - 150) * display_scale.mV[VX]), + (S32)((window_center_y + vertical_offset - PAD) * display_scale.mV[VY]), + (S32)(340 * display_scale.mV[VX]), + (S32)((PAD * 2 + 10) * display_scale.mV[VY] * 2), + LLColor4(0.f, 0.f, 0.f, 0.7f) + ); + + LLFontGL* font = LLFontGL::getFontSansSerif(); + LLLocale locale(LLLocale::USER_LOCALE); + LLGLDepthTest gls_depth(GL_FALSE); + + auto renderTextWithShadow = [&](const std::string& text, F32 x, F32 y, const LLColor4& color) { + font->render(utf8str_to_wstring(text), 0, x + 1.f, y - 2.f, LLColor4::black, + LLFontGL::LEFT, LLFontGL::BASELINE, + LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX, 1000, nullptr); + font->render(utf8str_to_wstring(text), 0, x, y, color, + LLFontGL::LEFT, LLFontGL::BASELINE, + LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX, 1000, nullptr); + }; + + F32 base_y = (F32)(window_center_y + vertical_offset); + renderTextWithShadow(llformat("X: %.3f", vec.mV[VX]), window_center_x - 122.f, base_y, LLColor4(1.f, 0.5f, 0.5f, 1.f)); + renderTextWithShadow(llformat("Y: %.3f", vec.mV[VY]), window_center_x - 47.f, base_y, LLColor4(0.5f, 1.f, 0.5f, 1.f)); + renderTextWithShadow(llformat("Z: %.3f", vec.mV[VZ]), window_center_x + 28.f, base_y, LLColor4(0.5f, 0.5f, 1.f, 1.f)); + renderTextWithShadow(llformat("Δ: %.3f", mLastAngle * RAD_TO_DEG), window_center_x + 103.f, base_y, LLColor4(1.f, 0.65f, 0.f, 1.f)); + base_y += 20.f; + renderTextWithShadow(llformat("Joint: %s", mJoint->getName().c_str()), window_center_x - 130.f, base_y, LLColor4(1.f, 0.1f, 1.f, 1.f)); + renderTextWithShadow(llformat("Manip: %s", getManipPartString(mManipPart).c_str()), window_center_x + 30.f, base_y, LLColor4(1.f, 1.f, .1f, 1.f)); + } + gGL.popMatrix(); + + gViewerWindow->setup3DRender(); +} + +void FSManipRotateJoint::renderActiveRing( F32 radius, F32 width, const LLColor4& front_color, const LLColor4& back_color) +{ + LLGLEnable cull_face(GL_CULL_FACE); + { + gl_ring(radius, width, back_color, back_color * 0.5f, CIRCLE_STEPS, false); + gl_ring(radius, width, back_color, back_color * 0.5f, CIRCLE_STEPS, true); + } + { + LLGLDepthTest gls_depth(GL_FALSE); + gl_ring(radius, width, front_color, front_color * 0.5f, CIRCLE_STEPS, false); + gl_ring(radius, width, front_color, front_color * 0.5f, CIRCLE_STEPS, true); + } +} + + +// ------------------------------------- +// Overriding because the base uses mObjectSelection->getFirstMoveableObject(true) +// Not sure we use it though...TBC (see mouse down on part instead) +bool FSManipRotateJoint::handleMouseDown(S32 x, S32 y, MASK mask) +{ + if (!mJoint) + { + return false; + } + + // Highlight the manipulator as before. + highlightManipulators(x, y); + + if (mHighlightedPart != LL_NO_PART) + { + mManipPart = (EManipPart)mHighlightedPart; + + // Get the joint's center in agent space. + LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + + // Use the existing function to get the intersection point. + LLVector3 intersection = intersectMouseWithSphere(x, y, agent_space_center, mRadiusMeters); + + // Check if the returned intersection is valid. + if (intersection.isExactlyZero()) + { + // Treat this as a "raycast miss" and do not capture the mouse. + return false; + } + else + { + // Save the valid intersection point. + mInitialIntersection = intersection; + // Also store the joint's current rotation. + mSavedJointRot = mJoint->getWorldRotation(); + + // Capture the mouse for dragging. + setMouseCapture(true); + return true; + } + } + return false; +} + +/** + * @brief Handles the mouse down event on a manipulator part. + * + * Along with render, this is the main top-level entry. + * This function determines which manipulator part (ring/axis) is under the mouse cursor + * using the highlightManipulator() function and highlights the selectable joints. + * It then saves the joint's current world rotation as the basis for the drag operation + * and sets the appropriate manipulation part. + * Depending on the manipulation part, it either performs an unconstrained rotation + * or a constrained rotation based on the axis. + * + * @param x The x-coordinate of the mouse cursor. + * @param y The y-coordinate of the mouse cursor. + * @param mask The mask indicating the state of modifier keys. + * @return true if the mouse down event is handled successfully, false otherwise. + */ +bool FSManipRotateJoint::handleMouseDownOnPart(S32 x, S32 y, MASK mask) +{ + // Determine which ring (axis) is under the mouse, also highlights selectable joints. + highlightManipulators(x, y); + // For joint manipulation, require both a valid joint and avatar. + if (!mJoint || !mAvatar) + { + return false; + } + + S32 hit_part = mHighlightedPart; + + // Save the joint’s current world rotation as the basis for the drag. + mSavedJointRot = mJoint->getWorldRotation(); + + mManipPart = (EManipPart)hit_part; + + // Convert rotation center from global to agent space. + LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + + // based on mManipPArt (set in highlightmanipulators). decide whether we are constrained or not in the rotation + if (mManipPart == LL_ROT_GENERAL) + { + // Unconstrained rotation. we use the intersection point as the mouse down point. + mMouseDown = intersectMouseWithSphere(x, y, agent_space_center, mRadiusMeters); + mInitialIntersection = mMouseDown; // Save the initial sphere intersection. + } + else + { + // Constrained rotation. + LLVector3 axis = setConstraintAxis(); // set the axis based on the manipulator part + F32 axis_onto_cam = llabs( axis * mCenterToCamNorm ); + const F32 AXIS_ONTO_CAM_TOL = cos( 85.f * DEG_TO_RAD ); + if( axis_onto_cam < AXIS_ONTO_CAM_TOL ) + { + LLVector3 up_from_axis = mCenterToCamNorm % axis; + up_from_axis.normalize(); + LLVector3 cur_intersection; + getMousePointOnPlaneAgent(cur_intersection, x, y, agent_space_center, mCenterToCam); + cur_intersection -= agent_space_center; + mMouseDown = projected_vec(cur_intersection, up_from_axis); + F32 mouse_depth = SNAP_GUIDE_INNER_RADIUS * mRadiusMeters; + F32 mouse_dist_sqrd = mMouseDown.magVecSquared(); + if (mouse_dist_sqrd > 0.0001f) + { + mouse_depth = sqrtf((SNAP_GUIDE_INNER_RADIUS * mRadiusMeters) * + (SNAP_GUIDE_INNER_RADIUS * mRadiusMeters) - mouse_dist_sqrd); + } + LLVector3 projected_center_to_cam = mCenterToCamNorm - projected_vec(mCenterToCamNorm, axis); + mMouseDown += mouse_depth * projected_center_to_cam; + } + else + { + mMouseDown = findNearestPointOnRing(x, y, agent_space_center, axis) - agent_space_center; + mMouseDown.normalize(); + } + mInitialIntersection = mMouseDown; + } + + // Set the current mouse vector equal to the initial one. + mMouseCur = mMouseDown; + + // Save the agent’s “at” axis (this might be used later in drag calculations). + mAgentSelfAtAxis = gAgent.getAtAxis(); + + // Capture the mouse so that subsequent mouse drag events are routed here. + setMouseCapture(true); + + // (Optionally, reset any help text timer or related UI feedback.) + mHelpTextTimer.reset(); + sNumTimesHelpTextShown++; + + return true; +} + +// We use mouseUp to update the UI, updating it during the drag is too slow. +bool FSManipRotateJoint::handleMouseUp(S32 x, S32 y, MASK mask) +{ + + auto * poser = dynamic_cast(LLFloaterReg::findInstance("fs_poser")); + 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) + { + poser->updatePosedBones(); + } + + // Release mouse + setMouseCapture(false); + mManipPart = LL_NO_PART; + mLastAngle = 0.0f; + return true; + } + else if(mHighlightedJoint) + { + if (poser) + { + poser->selectJointByName(mHighlightedJoint->getName()); + } + return true; + } + return false; +} + + + +/** + * @brief Does all the hard work of working out what inworld control we are interacting with + * + * There's quite a bit of overlap with the base class. + * Sadly the base is built around object selection, so we need to override. + * We also take this opportunity to highlight nearby joints that we might want to manipulate. + * + * @param x mouse x-coordinate + * @param y mouse y-coordinate + */ +void FSManipRotateJoint::highlightManipulators(S32 x, S32 y) +{ + // Clear any previous highlight. + mHighlightedPart = LL_NO_PART; + // Instead of using mObjectSelection->getFirstMoveableObject(), + // simply require that the joint (and the avatar) is valid. + if (!mJoint || !mAvatar) + { + highlightHoverSpheres(x, y); + gViewerWindow->setCursor(UI_CURSOR_ARROW); + return; + } + + // Decide which rotation to use based on a user toggle. + LLCachedControl use_natural_direction(gSavedSettings, "FSManipRotateJointUseNaturalDirection", true); + // Compute the rotation center in agent space. + LLVector3 agent_space_rotation_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + + // Update joint world matrices. + mJoint->updateWorldMatrixParent(); + mJoint->updateWorldMatrix(); + + const LLQuaternion joint_world_rotation = mJoint->getWorldRotation(); + + const LLQuaternion parentWorldRot = (mJoint->getParent()) ? mJoint->getParent()->getWorldRotation() : LLQuaternion::DEFAULT; + + LLQuaternion currentLocalRot = mJoint->getRotation(); + + LLQuaternion rotatedNaturalAlignment = mNaturalAlignmentQuat * currentLocalRot; + rotatedNaturalAlignment.normalize(); + // Compute the final world alignment: + LLQuaternion final_world_alignment = rotatedNaturalAlignment * parentWorldRot; + final_world_alignment.normalize(); + + + LLQuaternion joint_rot = use_natural_direction ? final_world_alignment : joint_world_rotation; + + // Compute the three local axes in world space. + LLVector3 rot_x_axis = LLVector3::x_axis * joint_rot; + LLVector3 rot_y_axis = LLVector3::y_axis * joint_rot; + LLVector3 rot_z_axis = LLVector3::z_axis * joint_rot; + + // mCenterToCamNorm is assumed to be computed already (for example in updateVisibility) + F32 proj_rot_x_axis = llabs(rot_x_axis * mCenterToCamNorm); + F32 proj_rot_y_axis = llabs(rot_y_axis * mCenterToCamNorm); + F32 proj_rot_z_axis = llabs(rot_z_axis * mCenterToCamNorm); + + // Variables to help choose the best candidate. + F32 min_select_distance = 0.f; + F32 cur_select_distance = 0.f; + + // These vectors will hold the intersection points on planes defined by each axis. + LLVector3 mouse_dir_x, mouse_dir_y, mouse_dir_z, intersection_roll; + + // For each axis, compute the mouse intersection on a plane passing through the rotation center. + getMousePointOnPlaneAgent(mouse_dir_x, x, y, agent_space_rotation_center, rot_x_axis); + mouse_dir_x -= agent_space_rotation_center; + mouse_dir_x *= 1.f + (1.f - llabs(rot_x_axis * mCenterToCamNorm)) * 0.1f; + + getMousePointOnPlaneAgent(mouse_dir_y, x, y, agent_space_rotation_center, rot_y_axis); + mouse_dir_y -= agent_space_rotation_center; + mouse_dir_y *= 1.f + (1.f - llabs(rot_y_axis * mCenterToCamNorm)) * 0.1f; + + getMousePointOnPlaneAgent(mouse_dir_z, x, y, agent_space_rotation_center, rot_z_axis); + mouse_dir_z -= agent_space_rotation_center; + mouse_dir_z *= 1.f + (1.f - llabs(rot_z_axis * mCenterToCamNorm)) * 0.1f; + + // For roll, intersect with a plane defined by the camera’s direction. + getMousePointOnPlaneAgent(intersection_roll, x, y, agent_space_rotation_center, mCenterToCamNorm); + intersection_roll -= agent_space_rotation_center; + + // Compute the distances (in agent-space) from the rotation center. + F32 dist_x = mouse_dir_x.normalize(); + F32 dist_y = mouse_dir_y.normalize(); + F32 dist_z = mouse_dir_z.normalize(); + + // Compute a threshold for selection. + F32 distance_threshold = (MAX_MANIP_SELECT_DISTANCE * mRadiusMeters) / gViewerWindow->getWorldViewHeightScaled(); + + // Define a lambda to test an axis ring. This captures variables by reference. + auto testAxisRing = [&](F32 dist, const LLVector3& mouse_dir, F32 proj_factor, LLManip::e_manip_part ringId) { + F32 absDiff = llabs(dist - mRadiusMeters); + // Instead of multiplying by proj_factor, we divide the threshold by it, + // so that near edge-on views (small proj_factor) yield a larger tolerance. + if (absDiff < distance_threshold / llmax(0.05f, proj_factor)) + { + F32 cur_select_distance = dist * (mouse_dir * mCenterToCamNorm); + if (cur_select_distance >= -0.05f && (min_select_distance == 0.f || cur_select_distance > min_select_distance)) + { + min_select_distance = cur_select_distance; + mHighlightedPart = ringId; + } + } + }; + + // Use the lambda for each axis. + testAxisRing(dist_x, mouse_dir_x, proj_rot_x_axis, LL_ROT_X); + testAxisRing(dist_y, mouse_dir_y, proj_rot_y_axis, LL_ROT_Y); + testAxisRing(dist_z, mouse_dir_z, proj_rot_z_axis, LL_ROT_Z); + + // --- Additional tests for edge-on intersections --- + if (proj_rot_x_axis < 0.05f) + { + if ((proj_rot_y_axis > 0.05f && (dist_y * llabs(mouse_dir_y * rot_x_axis) < distance_threshold) && dist_y < mRadiusMeters) || + (proj_rot_z_axis > 0.05f && (dist_z * llabs(mouse_dir_z * rot_x_axis) < distance_threshold) && dist_z < mRadiusMeters)) + { + mHighlightedPart = LL_ROT_X; + } + } + if (proj_rot_y_axis < 0.05f) + { + if ((proj_rot_x_axis > 0.05f && (dist_x * llabs(mouse_dir_x * rot_y_axis) < distance_threshold) && dist_x < mRadiusMeters) || + (proj_rot_z_axis > 0.05f && (dist_z * llabs(mouse_dir_z * rot_y_axis) < distance_threshold) && dist_z < mRadiusMeters)) + { + mHighlightedPart = LL_ROT_Y; + } + } + if (proj_rot_z_axis < 0.05f) + { + if ((proj_rot_x_axis > 0.05f && (dist_x * llabs(mouse_dir_x * rot_z_axis) < distance_threshold) && dist_x < mRadiusMeters) || + (proj_rot_y_axis > 0.05f && (dist_y * llabs(mouse_dir_y * rot_z_axis) < distance_threshold) && dist_y < mRadiusMeters)) + { + mHighlightedPart = LL_ROT_Z; + } + } + + // --- Test for roll if no primary axis was highlighted --- + if (mHighlightedPart == LL_NO_PART) + { + F32 roll_distance = intersection_roll.magVec(); + F32 width_meters = WIDTH_PIXELS * mRadiusMeters / RADIUS_PIXELS; + + if (llabs(roll_distance - (mRadiusMeters + (width_meters * 2.f))) < distance_threshold * 2.f) + { + mHighlightedPart = LL_ROT_ROLL; + } + else if (roll_distance < mRadiusMeters) + { + mHighlightedPart = LL_ROT_GENERAL; + } + } + + // If nothing else of interest then test for nearby joints we can select. + if (mHighlightedPart == LL_NO_PART) + { + highlightHoverSpheres(x, y); + gViewerWindow->setCursor(UI_CURSOR_ARROW); + } + else + { + gViewerWindow->setCursor(UI_CURSOR_TOOLROTATE); + } +} + + +// ------------------------------------- +bool FSManipRotateJoint::handleHover(S32 x, S32 y, MASK mask) +{ + // If we are dragging (hasMouseCapture), + // we do the "drag" logic but apply rotation to the joint + if (hasMouseCapture() && mJoint) + { + drag(x, y); // calls dragConstrained() or dragUnconstrained() + // but in drag(), we must override it so the final rotation + // is applied to the joint instead of an LLViewerObject + gViewerWindow->setCursor(UI_CURSOR_TOOLROTATE); + } + else + { + highlightManipulators(x, y); + } + return true; +} + +LLQuaternion FSManipRotateJoint::dragUnconstrained(S32 x, S32 y) +{ + // Get the camera position and the joint’s pivot (in agent space) + LLVector3 cam = gAgentCamera.getCameraPositionAgent(); + LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + + // Compute the current intersection on the sphere. + mMouseCur = intersectMouseWithSphere(x, y, agent_space_center, mRadiusMeters); + + // Use the screen center (set in updateVisibility) to compute how far + // the mouse is from the sphere’s center in screen space. + F32 delta_x = (F32)(mCenterScreen.mX - x); + F32 delta_y = (F32)(mCenterScreen.mY - y); + F32 dist_from_sphere_center = sqrtf(delta_x * delta_x + delta_y * delta_y); + + // Compute a rotation axis from the stored initial intersection to the current intersection. + LLVector3 axis = mInitialIntersection % mMouseCur; + F32 angle = atan2f(sqrtf(axis * axis), mInitialIntersection * mMouseCur); + axis.normalize(); + LLQuaternion sphere_rot(angle, axis); + + // If there is negligible change, return the identity. + if (is_approx_zero(1.f - mInitialIntersection * mMouseCur)) + { + return LLQuaternion::DEFAULT; + } + // If the mouse is still near the center of the manipulator in screen space, + // simply return the computed sphere rotation. + else if (dist_from_sphere_center < RADIUS_PIXELS) + { + return sphere_rot; + } + else + { + // Otherwise, compute an “extra” rotation based on a projection onto a profile plane. + LLVector3 intersection; + // Use the previously computed mCenterToProfilePlane and mCenterToCamNorm. + // This computes a point on the plane defined by (center + mCenterToProfilePlane) and oriented by mCenterToCamNorm. + getMousePointOnPlaneAgent(intersection, x, y, agent_space_center + mCenterToProfilePlane, mCenterToCamNorm); + + // Determine the “in-sphere” angle that corresponds to dragging from centre to periphery. + F32 in_sphere_angle = F_PI_BY_TWO; + F32 dist_to_tangent_point = mRadiusMeters; + if (!is_approx_zero(mCenterToProfilePlaneMag)) + { + dist_to_tangent_point = sqrtf(mRadiusMeters * mRadiusMeters - mCenterToProfilePlaneMag * mCenterToProfilePlaneMag); + in_sphere_angle = atan2f(dist_to_tangent_point, mCenterToProfilePlaneMag); + } + + LLVector3 profile_center_to_intersection = intersection - (agent_space_center + mCenterToProfilePlane); + F32 dist_to_intersection = profile_center_to_intersection.normalize(); + F32 extra_angle = (-1.f + dist_to_intersection / dist_to_tangent_point) * in_sphere_angle; + + // Compute a rotation axis from the camera-to-center vector and the profile difference. + axis = (cam - agent_space_center) % profile_center_to_intersection; + axis.normalize(); + + // Multiply the unconstrained sphere rotation with the extra rotation. + return sphere_rot * LLQuaternion(extra_angle, axis); + } +} +LLQuaternion FSManipRotateJoint::dragConstrained(S32 x, S32 y) +{ + // Get the constraint axis from our joint manipulator. + // (See the adjusted getConstraintAxis() below.) + LLVector3 constraint_axis = getConstraintAxis(); + LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + + // Project the current mouse position onto the plane defined by the constraint axis. + LLVector3 projected_mouse; + bool hit = getMousePointOnPlaneAgent(projected_mouse, x, y, agent_space_center, constraint_axis); + if (!hit) + { + return LLQuaternion::DEFAULT; + } + projected_mouse -= agent_space_center; + projected_mouse.normalize(); + + // Similarly, project the initial intersection (stored at mouse down) onto the same plane. + LLVector3 initial_proj = mInitialIntersection; + initial_proj -= (initial_proj * constraint_axis) * constraint_axis; + initial_proj.normalize(); + + // Compute the signed angle using atan2. + // The numerator is the magnitude of the cross product projected along the constraint axis. + float numerator = (initial_proj % projected_mouse) * constraint_axis; + // The denominator is the dot product. + float denominator = initial_proj * projected_mouse; + float angle = atan2(numerator, denominator); // angle in (-pi, pi) + mLastAngle = angle; + return LLQuaternion(angle, constraint_axis); +} +void FSManipRotateJoint::drag(S32 x, S32 y) +{ + if (!updateVisiblity() || !mJoint) return; + + LLQuaternion delta_rot; + if (mManipPart == LL_ROT_GENERAL) + { + delta_rot = dragUnconstrained(x, y); + } + else + { + delta_rot = dragConstrained(x, y); + } + + // Compose the saved joint rotation with the delta to compute the new world rotation. + LLQuaternion new_world_rot = mSavedJointRot * delta_rot; + mJoint->setWorldRotation(new_world_rot); +} + +// set mConstrainedAxis based on mManipParat and returns it too. +LLVector3 FSManipRotateJoint::setConstraintAxis() +{ + LLVector3 axis; + if (mManipPart == LL_ROT_ROLL) + { + axis = mCenterToCamNorm; + } + else + { + // For constrained rotations about X, Y, or Z: + // Assume mManipPart is defined such that LL_ROT_X, LL_ROT_Y, LL_ROT_Z correspond to 0, 1, 2. + S32 axis_dir = mManipPart - LL_ROT_X; + axis.setZero(); + if (axis_dir >= LL_NO_PART && axis_dir < LL_Z_ARROW) + { + axis.mV[axis_dir] = 1.f; + } + else + { + axis.mV[0] = 1.f; // Fallback to X. + } + // Transform the local axis into world space using the joint's world rotation. + if (mJoint) + { + LLCachedControl use_natural_direction(gSavedSettings, "FSManipRotateJointUseNaturalDirection", true); + LLQuaternion active_rotation; + if (use_natural_direction) + { + // Get the joint's current local rotation. + LLQuaternion currentLocalRot = mJoint->getRotation(); + const LLQuaternion parentWorldRot = (mJoint->getParent()) ? mJoint->getParent()->getWorldRotation() : LLQuaternion::DEFAULT; + LLQuaternion rotatedNaturalAlignment = mNaturalAlignmentQuat * currentLocalRot; + rotatedNaturalAlignment.normalize(); + LLQuaternion final_world_alignment = rotatedNaturalAlignment * parentWorldRot; + final_world_alignment.normalize(); + active_rotation = final_world_alignment; + } + else + { + active_rotation = mJoint->getWorldRotation(); + } + axis = axis * active_rotation; + axis.normalize(); + } + } + mConstraintAxis = axis; + return axis; +} diff --git a/indra/newview/fsmaniprotatejoint.h b/indra/newview/fsmaniprotatejoint.h new file mode 100644 index 0000000000..2c463e0685 --- /dev/null +++ b/indra/newview/fsmaniprotatejoint.h @@ -0,0 +1,121 @@ +// File: fsmaniprotatejoint.h + +#ifndef FS_MANIP_ROTATE_JOINT_H +#define FS_MANIP_ROTATE_JOINT_H + +#include "llmath.h" +#include "llmaniprotate.h" +#include "llselectmgr.h" +class LLJoint; +class LLVOAvatar; // or LLVOAvatarSelf, etc. +namespace { + const F32 AXIS_ONTO_CAM_TOLERANCE = cos( 80.f * DEG_TO_RAD ); // cos() is not constexpr til c++26 + constexpr F32 RADIUS_PIXELS = 100.f; // size in screen space + constexpr S32 CIRCLE_STEPS = 100; + constexpr F32 CIRCLE_STEP_SIZE = 2.0f * F_PI / CIRCLE_STEPS; + constexpr F32 SQ_RADIUS = RADIUS_PIXELS * RADIUS_PIXELS; + constexpr F32 WIDTH_PIXELS = 8; + constexpr F32 MAX_MANIP_SELECT_DISTANCE = 100.f; + constexpr F32 SNAP_ANGLE_INCREMENT = 5.625f; + constexpr F32 SNAP_ANGLE_DETENTE = SNAP_ANGLE_INCREMENT; + constexpr F32 SNAP_GUIDE_RADIUS_1 = 2.8f; + constexpr F32 SNAP_GUIDE_RADIUS_2 = 2.4f; + constexpr F32 SNAP_GUIDE_RADIUS_3 = 2.2f; + constexpr F32 SNAP_GUIDE_RADIUS_4 = 2.1f; + constexpr F32 SNAP_GUIDE_RADIUS_5 = 2.05f; + constexpr F32 SNAP_GUIDE_INNER_RADIUS = 2.f; + constexpr F32 SELECTED_MANIPULATOR_SCALE = 1.05f; + constexpr F32 MANIPULATOR_SCALE_HALF_LIFE = 0.07f; + constexpr S32 VERTICAL_OFFSET = 100; + +} +class FSManipRotateJoint : public LLManipRotate +{ + // Used for overriding the natural "up" direction of a joint. + // if no override is set then the world up direction is used unless the joint is vertical in which case we pick an arbitrary normal + static std::unordered_map sReferenceUpVectors; + + struct BoneAxes + { + LLVector3 naturalX; + LLVector3 naturalY; + LLVector3 naturalZ; + }; + LLQuaternion computeAlignmentQuat( const BoneAxes& boneAxes ) const; + BoneAxes computeBoneAxes() const; +public: + FSManipRotateJoint(LLToolComposite* composite); + virtual ~FSManipRotateJoint() {} + static std::string getManipPartString(EManipPart part); + // Called to designate which joint we are going to manipulate. + void setJoint(LLJoint* joint); + + void setAvatar(LLVOAvatar* avatar); + + // Overrides + void handleSelect() override; + bool updateVisiblity(); + void render() override; + void renderNameXYZ(const LLVector3 &vec); + bool handleMouseDown(S32 x, S32 y, MASK mask) override; + bool handleMouseUp(S32 x, S32 y, MASK mask) override; + bool handleHover(S32 x, S32 y, MASK mask) override; + void drag(S32 x, S32 y) override; + bool isAlwaysRendered() override { return true; } + void highlightManipulators(S32 x, S32 y) override; + bool handleMouseDownOnPart(S32 x, S32 y, MASK mask) override; + void highlightHoverSpheres(S32 mouseX, S32 mouseY); +protected: + // void renderNameXYZ(const std::string name, const LLVector3 &vec); + LLQuaternion dragUnconstrained( S32 x, S32 y ); + LLQuaternion dragConstrained( S32 x, S32 y ); + LLVector3 getConstraintAxis() const { return mConstraintAxis; }; + LLVector3 setConstraintAxis(); + bool computeMouseIntersectionOnSphere(S32 x, S32 y, + const LLVector3d& sphere_center_global, + F32 sphere_radius, + LLVector3& outIntersection); + + // Instead of selecting an LLViewerObject, we have a single joint + LLJoint* mJoint = nullptr; + BoneAxes mBoneAxes; + LLQuaternion mNaturalAlignmentQuat; + LLVOAvatar* mAvatar = nullptr; + // We'll store the joint's original rotation for reference + LLQuaternion mSavedJointRot; + LLJoint * mHighlightedJoint = nullptr; + F32 mHighlightedPartDistance = 0.f; + LLVector3 mInitialIntersection; // The initial point on the manipulator’s sphere (in agent space) + const std::vector getSelectableJoints(){ return sSelectableJoints; }; +private: + static const std::vector sSelectableJoints; + + // Structure holding parameters needed to render one manipulator ring. + struct RingRenderParams + { + EManipPart part; // e.g. LL_ROT_Z, LL_ROT_Y, LL_ROT_X, etc. + LLVector4 targetScale; // Target scale for mManipulatorScales for this part. + float extraRotateAngle; // Extra rotation angle (in degrees) to apply. + LLVector3 extraRotateAxis; // Axis for the extra rotation. + LLColor4 primaryColor; // Primary ring color. + LLColor4 secondaryColor; // Secondary ring color. + int scaleIndex; // Which component of mManipulatorScales to use (0: X, 1: Y, 2: Z, 3: Roll). + }; + + static const std::unordered_map sRingParams; + void updateManipulatorScale(EManipPart part, LLVector4& scales); + void renderActiveRing( F32 radius, F32 width, const LLColor4& front_color, const LLColor4& back_color); + void renderManipulatorRings(const LLVector3& center, const LLQuaternion& finalAlignment); + + void renderCenterCircle(const F32 radius, const LLColor4 normal_color=LLColor4(0.7f,0.7,0.7f,0.2), const LLColor4 highlight_color=LLColor4(0.8f,0.8f,0.8f,0.3)); + void renderCenterSphere(const F32 radius, const LLColor4 normal_color=LLColor4(0.7f,0.7,0.7f,0.2), const LLColor4 highlight_color=LLColor4(0.8f,0.8f,0.8f,0.3)); + void renderDetailedRings(float width_meters); + void renderRingPass(const RingRenderParams& params, float radius, float width, int pass); + void renderAxes(const LLVector3& center, F32 size, const LLQuaternion& rotation); + + float mLastAngle = 0.f; + LLVector3 mConstraintAxis; + +}; + +#endif // FS_MANIP_ROTATE_JOINT_H diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index 2aabb98985..39938d4490 100644 --- a/indra/newview/fsposeranimator.cpp +++ b/indra/newview/fsposeranimator.cpp @@ -322,6 +322,22 @@ 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) +{ + if (!isAvatarSafeToUse(avatar)) + return; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return; + + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); + if (!jointPose) + return; + + jointPose->recaptureJointAsDelta(); + setPosingAvatarJoint(avatar, joint, true); +} 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 cf6652a62f..045558288a 100644 --- a/indra/newview/fsposeranimator.h +++ b/indra/newview/fsposeranimator.h @@ -509,12 +509,14 @@ public: /// /// 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. /// /// The avatar whose joint is to be recaptured. /// The joint to recapture. /// 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); /// /// Sets all of the joint rotations of the supplied avatar to zero. diff --git a/indra/newview/llmaniprotate.h b/indra/newview/llmaniprotate.h index fa764a9ced..c0c83c305a 100644 --- a/indra/newview/llmaniprotate.h +++ b/indra/newview/llmaniprotate.h @@ -40,6 +40,7 @@ class LLColor4; class LLManipRotate : public LLManip { + friend class FSManipRotateJoint; public: class ManipulatorHandle { @@ -67,13 +68,13 @@ public: private: void updateHoverView(); - void drag( S32 x, S32 y ); + virtual void drag( S32 x, S32 y ); LLVector3 projectToSphere( F32 x, F32 y, bool* on_sphere ); void renderSnapGuides(); void renderActiveRing(F32 radius, F32 width, const LLColor4& center_color, const LLColor4& side_color); - bool updateVisiblity(); + virtual bool updateVisiblity(); LLVector3 findNearestPointOnRing( S32 x, S32 y, const LLVector3& center, const LLVector3& axis ); LLQuaternion dragUnconstrained( S32 x, S32 y ); diff --git a/indra/newview/lltoolcomp.cpp b/indra/newview/lltoolcomp.cpp index e393a84b3c..b5aa087962 100644 --- a/indra/newview/lltoolcomp.cpp +++ b/indra/newview/lltoolcomp.cpp @@ -899,3 +899,165 @@ bool LLToolCompGun::handleScrollWheel(S32 x, S32 y, S32 clicks) } return true; } + + +#include "llviewerwindow.h" // for gViewerWindow->pickAsync() +#include "llselectmgr.h" // for LLSelectMgr +#include "llfloaterreg.h" // for LLFloaterReg::showInstance() +#include "llviewermenu.h" // for LLEditMenuHandler::gEditMenuHandler +#include "fsfloaterposer.h" + +// If you want a standard static instance approach: +FSToolCompPose* FSToolCompPose::getInstance() +{ + // Meyers singleton pattern + static FSToolCompPose instance; + return &instance; +} + +//----------------------------------- +// Constructor +FSToolCompPose::FSToolCompPose() +: LLToolComposite(std::string("Pose")) +{ + // Create a joint manipulator + mManip = new FSManipRotateJoint(this); + + // Possibly create a selection rectangle tool if you want + // to be able to box-select joints or objects + // (same usage as LLToolCompRotate does) + // mSelectRect = new LLToolSelectRect(this); + + // Set the default and current subtool + mCur = mManip; + mDefault = mManip; +} + +//----------------------------------- +// Destructor +FSToolCompPose::~FSToolCompPose() +{ + delete mManip; + mManip = nullptr; + + delete mSelectRect; + mSelectRect = nullptr; +} + +//----------------------------------- +// Handle Hover +bool FSToolCompPose::handleHover(S32 x, S32 y, MASK mask) +{ + // If the current subtool hasn't captured the mouse, + // switch to your manip subtool (like LLToolCompRotate). + if (!mCur->hasMouseCapture()) + { + setCurrentTool(mManip); + } + return mCur->handleHover(x, y, mask); +} + +//----------------------------------- +// Handle MouseDown +bool FSToolCompPose::handleMouseDown(S32 x, S32 y, MASK mask) +{ + mMouseDown = true; + + // Kick off an async pick, which calls pickCallback when complete + // so we can see if user clicked on a manip ring or not + gViewerWindow->pickAsync(x, y, mask, pickCallback); + return true; +} + +//----------------------------------- +// The pickCallback +void FSToolCompPose::pickCallback(const LLPickInfo& pick_info) +{ + FSToolCompPose* self = FSToolCompPose::getInstance(); + FSManipRotateJoint* manip = self->mManip; + + if (!manip) return; // No manipulator available, exit + + // Highlight the manipulator based on the mouse position + manip->highlightManipulators(pick_info.mMousePt.mX, pick_info.mMousePt.mY); + + if (!self->mMouseDown) + { + // No action needed if mouse is up; interaction is handled by highlight logic + return; + } + + // Check if a manipulator ring is highlighted + if (manip->getHighlightedPart() != LLManip::LL_NO_PART) + { + // Switch to the manipulator tool for dragging + self->setCurrentTool(manip); + manip->handleMouseDownOnPart(pick_info.mMousePt.mX, pick_info.mMousePt.mY, pick_info.mKeyMask); + } + else + { + // If no ring is highlighted, reset interaction or do nothing + LL_INFOS("FSToolCompPose") << "No manipulator ring selected" << LL_ENDL; + } +} + + +//----------------------------------- +// Handle MouseUp +bool FSToolCompPose::handleMouseUp(S32 x, S32 y, MASK mask) +{ + mMouseDown = false; + // The base LLToolComposite sets mCur->handleMouseUp(...) + // and does other management + return LLToolComposite::handleMouseUp(x, y, mask); +} + +//----------------------------------- +// getOverrideTool +// If you want SHIFT+CTRL combos to do something else +LLTool* FSToolCompPose::getOverrideTool(MASK mask) +{ + // Example from LLToolCompRotate that calls scale if SHIFT+CTRL + if (mask == (MASK_CONTROL | MASK_SHIFT)) + { + // If you have a scale tool, return that. Or else remove + // this if you don't want an override. + return LLToolCompScale::getInstance(); + } + // Otherwise fallback + return LLToolComposite::getOverrideTool(mask); +} + +//----------------------------------- +// Handle DoubleClick +bool FSToolCompPose::handleDoubleClick(S32 x, S32 y, MASK mask) +{ + if (!mManip->getSelection()->isEmpty() && + mManip->getHighlightedPart() == LLManip::LL_NO_PART) + { + // Possibly show some pose properties or open the pose floater + mPoser = dynamic_cast(LLFloaterReg::showInstance("fs_poser")); + return true; + } + else + { + // If nothing selected, try a mouse down again + return handleMouseDown(x, y, mask); + } +} + +//----------------------------------- +// render +void FSToolCompPose::render() +{ + // Render the current subtool + mCur->render(); + + // If the current subtool is not the manip, we can still + // optionally draw manip guidelines in the background + if (mCur != mManip) + { + mManip->renderGuidelines(); // or something similar if your manip has it + LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE); + } +} diff --git a/indra/newview/lltoolcomp.h b/indra/newview/lltoolcomp.h index ca133a7b6a..fbce6f9079 100644 --- a/indra/newview/lltoolcomp.h +++ b/indra/newview/lltoolcomp.h @@ -252,5 +252,44 @@ protected: LLTool* mNull; }; +// Subclass of LLToolComposite +#include "fsmaniprotatejoint.h" // For FSManipRotateJoint +#include "fsfloaterposer.h" +class FSToolCompPose : public LLToolComposite +{ +public: + // Typical pattern: pass a name like "Pose" + FSToolCompPose(); + virtual ~FSToolCompPose(); + + // For some viewer patterns, we create a static singleton: + static FSToolCompPose* getInstance(); + + // Overriding base events: + virtual bool handleHover(S32 x, S32 y, MASK mask) override; + virtual bool handleMouseDown(S32 x, S32 y, MASK mask) override; + virtual bool handleMouseUp(S32 x, S32 y, MASK mask) override; + virtual bool handleDoubleClick(S32 x, S32 y, MASK mask) override; + virtual void render() override; + void setAvatar(LLVOAvatar* avatar) { mManip->setAvatar(avatar); }; + void setJoint( LLJoint * joint ) { mManip->setJoint( joint ); }; + + // Optional override if you have SHIFT/CTRL combos + virtual LLTool* getOverrideTool(MASK mask) override; + + // The pick callback invoked on async pick + static void pickCallback(const LLPickInfo& pick_info); + void setPoserFloater(FSFloaterPoser* poser){ mPoser = poser; }; + FSFloaterPoser* getPoserFloater(){ return mPoser; }; +protected: + // Tools within this composite + FSManipRotateJoint* mManip = nullptr; + LLToolSelectRect* mSelectRect= nullptr; + FSFloaterPoser* mPoser = nullptr; + + // Track mouse state similarly to LLToolCompRotate + bool mMouseDown = false; +}; + #endif // LL_TOOLCOMP_H diff --git a/indra/newview/lltoolmgr.cpp b/indra/newview/lltoolmgr.cpp index 77f2d11abc..25f211c2ac 100644 --- a/indra/newview/lltoolmgr.cpp +++ b/indra/newview/lltoolmgr.cpp @@ -70,6 +70,7 @@ LLToolset* gCameraToolset = NULL; //LLToolset* gLandToolset = NULL; LLToolset* gMouselookToolset = NULL; LLToolset* gFaceEditToolset = NULL; +LLToolset* gPoserToolset = nullptr; ///////////////////////////////////////////////////// // LLToolMgr @@ -99,6 +100,8 @@ LLToolMgr::LLToolMgr() // gLandToolset = new LLToolset(); gMouselookToolset = new LLToolset(); gFaceEditToolset = new LLToolset(); + gPoserToolset = new LLToolset(); + gPoserToolset->setShowFloaterTools(false); gMouselookToolset->setShowFloaterTools(false); gFaceEditToolset->setShowFloaterTools(false); } @@ -121,6 +124,7 @@ void LLToolMgr::initTools() gMouselookToolset->addTool( LLToolCompGun::getInstance() ); gBasicToolset->addTool( LLToolCompInspect::getInstance() ); gFaceEditToolset->addTool( LLToolCamera::getInstance() ); + gPoserToolset->addTool( FSToolCompPose::getInstance() ); // On startup, use "select" tool setCurrentToolset(gBasicToolset); diff --git a/indra/newview/lltoolmgr.h b/indra/newview/lltoolmgr.h index d321bbb477..2ba4b8c809 100644 --- a/indra/newview/lltoolmgr.h +++ b/indra/newview/lltoolmgr.h @@ -129,6 +129,7 @@ extern LLToolset *gCameraToolset; //extern LLToolset *gLandToolset; extern LLToolset* gMouselookToolset; extern LLToolset* gFaceEditToolset; +extern LLToolset* gPoserToolset; extern LLTool* gToolNull; diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 5d7f76efcd..386788bd99 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -5096,6 +5096,13 @@ void LLViewerWindow::renderSelections( bool for_gl_pick, bool pick_parcel_walls, // Call this once and only once LLSelectMgr::getInstance()->updateSilhouettes(); } + // render the poser manipulator guides + // if we have something selected those toosl should override + if ( (!for_hud) && (selection->isEmpty()) && (LLToolMgr::getInstance()->getCurrentTool() == FSToolCompPose::getInstance()) ) + { + FSToolCompPose::getInstance()->render(); + } + // // Draw fence around land selections if (for_gl_pick) diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 40c070515c..31ad8fd6d8 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -1934,6 +1934,227 @@ void LLVOAvatar::renderBones(const std::string &selected_joint) } } +void LLVOAvatar::renderOnlySelectedBones(const std::vector &selected_joints) +{ + LLGLEnable blend(GL_BLEND); + + avatar_joint_list_t::iterator iter = mSkeleton.begin(); + avatar_joint_list_t::iterator end = mSkeleton.end(); + + // For selected joints + static LLVector3 SELECTED_COLOR_OCCLUDED(1.0f, 1.0f, 0.0f); + static LLVector3 SELECTED_COLOR_VISIBLE(0.5f, 0.5f, 0.5f); + // For bones with position overrides defined + static LLVector3 OVERRIDE_COLOR_OCCLUDED(1.0f, 0.0f, 0.0f); + static LLVector3 OVERRIDE_COLOR_VISIBLE(0.5f, 0.5f, 0.5f); + // For bones which are rigged to by at least one attachment + static LLVector3 RIGGED_COLOR_OCCLUDED(0.0f, 1.0f, 1.0f); + static LLVector3 RIGGED_COLOR_VISIBLE(0.5f, 0.5f, 0.5f); + // For bones not otherwise colored + static LLVector3 OTHER_COLOR_OCCLUDED(0.0f, 1.0f, 0.0f); + static LLVector3 OTHER_COLOR_VISIBLE(0.5f, 0.5f, 0.5f); + + static F32 SPHERE_SCALEF = 0.001f; + + for (; iter != end; ++iter) + { + LLJoint* jointp = *iter; + if (!jointp) + { + continue; + } + if (std::find(selected_joints.begin(), selected_joints.end(), jointp->getName()) == selected_joints.end()) + { + continue; + } + jointp->updateWorldMatrix(); + + LLVector3 occ_color, visible_color; + + occ_color = SELECTED_COLOR_OCCLUDED; + visible_color = SELECTED_COLOR_VISIBLE; + + gGL.pushMatrix(); + gGL.multMatrix( &jointp->getXform()->getWorldMatrix().mMatrix[0][0] ); + + gGL.diffuseColor3f( 1.f, 0.f, 1.f ); + + gGL.begin(LLRender::LINES); + + LLVector3 v[] = + { + LLVector3(1,0,0), + LLVector3(-1,0,0), + LLVector3(0,1,0), + LLVector3(0,-1,0), + + LLVector3(0,0,-1), + LLVector3(0,0,1), + }; + + //sides + gGL.vertex3fv(v[0].mV); + gGL.vertex3fv(v[2].mV); + + gGL.vertex3fv(v[0].mV); + gGL.vertex3fv(v[3].mV); + + gGL.vertex3fv(v[1].mV); + gGL.vertex3fv(v[2].mV); + + gGL.vertex3fv(v[1].mV); + gGL.vertex3fv(v[3].mV); + + + //top + gGL.vertex3fv(v[0].mV); + gGL.vertex3fv(v[4].mV); + + gGL.vertex3fv(v[1].mV); + gGL.vertex3fv(v[4].mV); + + gGL.vertex3fv(v[2].mV); + gGL.vertex3fv(v[4].mV); + + gGL.vertex3fv(v[3].mV); + gGL.vertex3fv(v[4].mV); + + + //bottom + gGL.vertex3fv(v[0].mV); + gGL.vertex3fv(v[5].mV); + + gGL.vertex3fv(v[1].mV); + gGL.vertex3fv(v[5].mV); + + gGL.vertex3fv(v[2].mV); + gGL.vertex3fv(v[5].mV); + + gGL.vertex3fv(v[3].mV); + gGL.vertex3fv(v[5].mV); + + gGL.end(); + + gGL.popMatrix(); + + + // renderBoxAroundJointAttachments( jointp ); + + } + + + // // draw joint space bounding boxes of rigged attachments in yellow + // gGL.color3f(1.f, 1.f, 0.f); + // for (S32 joint_num = 0; joint_num < LL_CHARACTER_MAX_ANIMATED_JOINTS; joint_num++) + // { + // LLJoint* joint = getJoint(joint_num); + // LLJointRiggingInfo* rig_info = NULL; + // if (joint_num < mJointRiggingInfoTab.size()) + // { + // rig_info = &mJointRiggingInfoTab[joint_num]; + // } + + // if (joint && rig_info && rig_info->isRiggedTo()) + // { + // LLViewerJointAttachment* as_joint_attach = dynamic_cast(joint); + // if (as_joint_attach && as_joint_attach->getIsHUDAttachment()) + // { + // // Ignore bounding box of HUD joints + // continue; + // } + // gGL.pushMatrix(); + // gGL.multMatrix(&joint->getXform()->getWorldMatrix().mMatrix[0][0]); + + // LLVector4a pos; + // LLVector4a size; + + // const LLVector4a* extents = rig_info->getRiggedExtents(); + + // pos.setAdd(extents[0], extents[1]); + // pos.mul(0.5f); + // size.setSub(extents[1], extents[0]); + // size.mul(0.5f); + + // drawBoxOutline(pos, size); + + // gGL.popMatrix(); + // } + // } + + // draw world space attachment rigged bounding boxes in cyan + // gGL.color3f(0.f, 1.f, 1.f); + // for (attachment_map_t::iterator iter = mAttachmentPoints.begin(); + // iter != mAttachmentPoints.end(); + // ++iter) + // { + // LLViewerJointAttachment* attachment = iter->second; + + // if (attachment->getValid()) + // { + // for (LLViewerJointAttachment::attachedobjs_vec_t::iterator attachment_iter = attachment->mAttachedObjects.begin(); + // attachment_iter != attachment->mAttachedObjects.end(); + // ++attachment_iter) + // { + // LLViewerObject* attached_object = attachment_iter->get(); + // if (attached_object && !attached_object->isHUDAttachment()) + // { + // LLDrawable* drawable = attached_object->mDrawable; + // if (drawable && drawable->isState(LLDrawable::RIGGED | LLDrawable::RIGGED_CHILD)) + // { + // // get face rigged extents + // for (S32 i = 0; i < drawable->getNumFaces(); ++i) + // { + // LLFace* facep = drawable->getFace(i); + // if (facep && facep->isState(LLFace::RIGGED)) + // { + // LLVector4a center, size; + + // LLVector4a* extents = facep->mRiggedExtents; + + // center.setAdd(extents[0], extents[1]); + // center.mul(0.5f); + // size.setSub(extents[1], extents[0]); + // size.mul(0.5f); + // drawBoxOutline(center, size); + // } + // } + // } + // } + // } + // } + // } +} +void LLVOAvatar::renderBoxAroundJointAttachments(LLJoint * joint) +{ + LLJointRiggingInfo* rig_info = NULL; + + if (joint->getJointNum() < mJointRiggingInfoTab.size()) + { + rig_info = &mJointRiggingInfoTab[joint->getJointNum()]; + } + + if (joint && rig_info && rig_info->isRiggedTo()) + { + LLViewerJointAttachment* as_joint_attach = dynamic_cast(joint); + gGL.pushMatrix(); + gGL.multMatrix(&joint->getXform()->getWorldMatrix().mMatrix[0][0]); + + LLVector4a pos; + LLVector4a size; + + const LLVector4a* extents = rig_info->getRiggedExtents(); + + pos.setAdd(extents[0], extents[1]); + pos.mul(0.5f); + size.setSub(extents[1], extents[0]); + size.mul(0.5f); + + drawBoxOutline(pos, size); + + gGL.popMatrix(); + } +} + void LLVOAvatar::renderJoints() { diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index ee79a86187..70b8a5fba7 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -568,6 +568,9 @@ public: U32 renderTransparent(bool first_pass); void renderCollisionVolumes(); void renderBones(const std::string &selected_joint = std::string()); + void renderOnlySelectedBones(const std::vector &selected_joints); + void renderBoxAroundJointAttachments(LLJoint * joint); + void renderJoints(); static void deleteCachedImages(bool clearAll=true); static void destroyGL(); From 2728c252f487cb6e251856234b82ef889fdf10fe Mon Sep 17 00:00:00 2001 From: Beq Date: Fri, 21 Feb 2025 20:24:31 +0000 Subject: [PATCH 5/9] Make Linux happy about header files --- indra/newview/fsfloaterposer.cpp | 1 + indra/newview/fsmaniprotatejoint.cpp | 14 +++++++++++++- indra/newview/fsmaniprotatejoint.h | 4 +--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 48a4144eb8..48386e902e 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -27,6 +27,7 @@ #include "fsfloaterposer.h" #include "fsposeranimator.h" #include "fsvirtualtrackpad.h" +#include "v4color.h" #include "llagent.h" #include "llavatarnamecache.h" #include "llcheckboxctrl.h" diff --git a/indra/newview/fsmaniprotatejoint.cpp b/indra/newview/fsmaniprotatejoint.cpp index 255d0f369d..6ab5724073 100644 --- a/indra/newview/fsmaniprotatejoint.cpp +++ b/indra/newview/fsmaniprotatejoint.cpp @@ -25,7 +25,19 @@ * $/LicenseInfo$ */ -#include "fsmaniprotatejoint.h" + #include "llviewerprecompiledheaders.h" + + #include "fsmaniprotatejoint.h" + +// library includes +#include "llmath.h" +#include "llgl.h" +#include "llrender.h" +#include "v4color.h" +#include "llprimitive.h" +#include "llview.h" +#include "llfontgl.h" + #include "llrendersphere.h" #include "llvoavatar.h" #include "lljoint.h" diff --git a/indra/newview/fsmaniprotatejoint.h b/indra/newview/fsmaniprotatejoint.h index 2c463e0685..5914d1fb11 100644 --- a/indra/newview/fsmaniprotatejoint.h +++ b/indra/newview/fsmaniprotatejoint.h @@ -2,10 +2,8 @@ #ifndef FS_MANIP_ROTATE_JOINT_H #define FS_MANIP_ROTATE_JOINT_H - -#include "llmath.h" -#include "llmaniprotate.h" #include "llselectmgr.h" +#include "llmaniprotate.h" class LLJoint; class LLVOAvatar; // or LLVOAvatarSelf, etc. namespace { From fdc848160d679eeb69ec891c4a7c3be3d7aaa3f2 Mon Sep 17 00:00:00 2001 From: Beq Date: Fri, 21 Feb 2025 23:02:13 +0000 Subject: [PATCH 6/9] Fix lambda to keep MacOS happy --- indra/newview/fsfloaterposer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 48386e902e..7bc29e34f3 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -220,7 +220,7 @@ bool FSFloaterPoser::postBuild() mBtnJointRotate = getChild("button_joint_rotate_tool"); mCommitCallbackRegistrar.add("Poser.SetRotateTool", - [this](LLUICtrl*, const LLSD&) + [](LLUICtrl*, const LLSD&) { LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); LLToolMgr::getInstance()->getCurrentToolset()->selectTool( (LLTool *) FSToolCompPose::getInstance()); From 3fd6a54fc5715740edfbb182ebc02b24c7a90d13 Mon Sep 17 00:00:00 2001 From: Beq Date: Thu, 13 Mar 2025 00:49:23 +0000 Subject: [PATCH 7/9] Change CrashContext file name per new BugSplat update --- indra/newview/llappviewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 42a3fdd5de..d05e69399e 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -771,7 +771,7 @@ LLAppViewer::LLAppViewer() // static_debug_info.log file std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, ""); // Improve Bugsplat tracking by using attributes - BugSplatAttributes::setCrashContextFileName(logdir + "crash-context.xml"); + BugSplatAttributes::setCrashContextFileName(logdir + "CrashContext.xml"); // # else // ! LL_BUGSPLAT // write Google Breakpad minidump files to a per-run dump directory to avoid multiple viewer issues. From 4130e9a41646b3455a6ef491d3e1f8326311ef80 Mon Sep 17 00:00:00 2001 From: Beq Date: Thu, 13 Mar 2025 00:51:41 +0000 Subject: [PATCH 8/9] Allow toggle of visual manipulators. --- indra/newview/fsfloaterposer.cpp | 53 ++++++++++++------ indra/newview/fsfloaterposer.h | 5 ++ indra/newview/fsjointpose.cpp | 12 ++-- indra/newview/lltoolcomp.cpp | 2 +- .../textures/icons/visual_pose_enabled.png | Bin 0 -> 33593 bytes .../skins/default/textures/textures.xml | 5 ++ .../skins/default/xui/en/floater_fs_poser.xml | 20 ++++++- 7 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 indra/newview/skins/default/textures/icons/visual_pose_enabled.png diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 7bc29e34f3..7fe558734f 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -75,6 +75,7 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) mCommitCallbackRegistrar.add("Poser.RefreshAvatars", [this](LLUICtrl*, const LLSD&) { onAvatarsRefresh(); }); mCommitCallbackRegistrar.add("Poser.StartStopAnimating", [this](LLUICtrl*, const LLSD&) { onPoseStartStop(); }); mCommitCallbackRegistrar.add("Poser.ToggleLoadSavePanel", [this](LLUICtrl*, const LLSD&) { onToggleLoadSavePanel(); }); + mCommitCallbackRegistrar.add("Poser.ToggleVisualManipulators", [this](LLUICtrl*, const LLSD&) { onToggleVisualManipulators(); }); mCommitCallbackRegistrar.add("Poser.UndoLastRotation", [this](LLUICtrl*, const LLSD&) { onUndoLastChange(); }); mCommitCallbackRegistrar.add("Poser.RedoLastRotation", [this](LLUICtrl*, const LLSD&) { onRedoLastChange(); }); @@ -149,6 +150,9 @@ bool FSFloaterPoser::postBuild() mPosesScrollList->setCommitOnSelectionChange(true); mPosesScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onPoseFileSelect(); }); + mToggleVisualManipulators = getChild("toggleVisualManipulators"); + mToggleVisualManipulators->setToggleState(true); + mTrackpadSensitivitySlider = getChild("trackpad_sensitivity_slider"); mPoseSaveNameEditor = getChild("pose_save_name"); @@ -219,18 +223,6 @@ bool FSFloaterPoser::postBuild() mBtnJointRotate = getChild("button_joint_rotate_tool"); - mCommitCallbackRegistrar.add("Poser.SetRotateTool", - [](LLUICtrl*, const LLSD&) - { - LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); - LLToolMgr::getInstance()->getCurrentToolset()->selectTool( (LLTool *) FSToolCompPose::getInstance()); - } - ); - - LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); - LLToolMgr::getInstance()->getCurrentToolset()->selectTool( (LLTool *) FSToolCompPose::getInstance()); - FSToolCompPose::getInstance()->setAvatar( gAgentAvatarp); - return true; } @@ -242,6 +234,13 @@ void FSFloaterPoser::onOpen(const LLSD& key) onJointTabSelect(); refreshPoseScroll(mHandPresetsScrollList, POSE_PRESETS_HANDS_SUBDIRECTORY); startPosingSelf(); + + enableVisualManipulators(); + LLFloater::onOpen(key); +} + +void FSFloaterPoser::enableVisualManipulators() +{ if (LLToolMgr::getInstance()->getCurrentToolset() != gCameraToolset) { mLastToolset = LLToolMgr::getInstance()->getCurrentToolset(); @@ -249,19 +248,25 @@ void FSFloaterPoser::onOpen(const LLSD& key) LLToolMgr::getInstance()->setCurrentToolset(gPoserToolset); LLToolMgr::getInstance()->getCurrentToolset()->selectTool(FSToolCompPose::getInstance()); FSToolCompPose::getInstance()->setAvatar( gAgentAvatarp); - LLFloater::onOpen(key); } -void FSFloaterPoser::onClose(bool app_quitting) +void FSFloaterPoser::disableVisualManipulators() { - if (gSavedSettings.getBOOL(POSER_STOPPOSINGWHENCLOSED_SAVE_KEY)) - stopPosingAllAvatars(); - if (mLastToolset) { LLToolMgr::getInstance()->setCurrentToolset(mLastToolset); } FSToolCompPose::getInstance()->setAvatar(nullptr); +} + +void FSFloaterPoser::onClose(bool app_quitting) +{ + if (gSavedSettings.getBOOL(POSER_STOPPOSINGWHENCLOSED_SAVE_KEY)) + { + stopPosingAllAvatars(); + } + + disableVisualManipulators(); LLFloater::onClose(app_quitting); } @@ -1385,6 +1390,20 @@ void FSFloaterPoser::enableOrDisableRedoButton() mRedoChangeBtn->setEnabled(shouldEnableRedoButton); } +void FSFloaterPoser::onToggleVisualManipulators() +{ + bool tools_enabled = mToggleVisualManipulators->getValue().asBoolean(); + + if (tools_enabled) + { + enableVisualManipulators(); + } + else + { + disableVisualManipulators(); + } +} + void FSFloaterPoser::selectJointByName(const std::string& jointName) { LLTabContainer* tabContainer = mJointsTabs; diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 447dfbe5ec..cf75cd65c9 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -218,6 +218,9 @@ public: bool poseFileStartsFromTeePose(const std::string& poseFileName); void setPoseSaveFileTextBoxToUiSelectedAvatarSaveFileName(); void setUiSelectedAvatarSaveFileName(const std::string& saveFileName); + // visual manipulators control + void enableVisualManipulators(); + void disableVisualManipulators(); // UI Event Handlers: void onAvatarsRefresh(); @@ -225,6 +228,7 @@ public: void onJointTabSelect(); void onToggleMirrorChange(); void onToggleSympatheticChange(); + void onToggleVisualManipulators(); void setRotationChangeButtons(bool mirror, bool sympathetic); void onUndoLastChange(); void onRedoLastChange(); @@ -469,6 +473,7 @@ public: LLScrollListCtrl* mPosesScrollList{ nullptr }; LLScrollListCtrl* mHandPresetsScrollList{ nullptr }; + LLButton* mToggleVisualManipulators{ nullptr }; LLButton* mStartStopPosingBtn{ nullptr }; LLButton* mToggleLoadSavePanelBtn{ nullptr }; LLButton* mBrowserFolderBtn{ nullptr }; diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index d30d0eb68c..53a6b2869e 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -147,14 +147,18 @@ void FSJointPose::recaptureJoint() void FSJointPose::recaptureJointAsDelta() { if (mIsCollisionVolume) + { return; - + } + LLJoint* joint = mJointState->getJoint(); if (!joint) + { return; - - addToUndo(mRotation, &mUndoneRotationIndex, &mLastSetRotationDeltas, &mTimeLastUpdatedRotation); - mRotation.updateRotation(joint->getRotation()); + } + + addStateToUndo(mCurrentState); + mCurrentState = FSJointState(joint); } void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) diff --git a/indra/newview/lltoolcomp.cpp b/indra/newview/lltoolcomp.cpp index b5aa087962..c4d3fbe167 100644 --- a/indra/newview/lltoolcomp.cpp +++ b/indra/newview/lltoolcomp.cpp @@ -997,7 +997,7 @@ void FSToolCompPose::pickCallback(const LLPickInfo& pick_info) else { // If no ring is highlighted, reset interaction or do nothing - LL_INFOS("FSToolCompPose") << "No manipulator ring selected" << LL_ENDL; + LL_DEBUGS("FSToolCompPose") << "No manipulator ring selected" << LL_ENDL; } } diff --git a/indra/newview/skins/default/textures/icons/visual_pose_enabled.png b/indra/newview/skins/default/textures/icons/visual_pose_enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..eee9a94477044f47995ec7af3877d5b17a748014 GIT binary patch literal 33593 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 + + + + + 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 83a0ccdd81..d9f809a8d7 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -925,6 +925,24 @@ width="430"> width="134" /> + - + \ No newline at end of file From eecf28896e1495cdaa24ce88a44b669714795ad5 Mon Sep 17 00:00:00 2001 From: Beq Date: Thu, 13 Mar 2025 12:46:56 +0000 Subject: [PATCH 9/9] Add undo/redo keyboard accelerator support to poser --- indra/newview/fsfloaterposer.cpp | 13 +++++++++++++ indra/newview/fsfloaterposer.h | 9 +++++++-- indra/newview/fsmaniprotatejoint.cpp | 5 +++-- .../skins/default/xui/en/floater_fs_poser.xml | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 7fe558734f..088ab6b0ae 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -239,6 +239,19 @@ void FSFloaterPoser::onOpen(const LLSD& key) LLFloater::onOpen(key); } + +void FSFloaterPoser::onFocusReceived() +{ + LLEditMenuHandler::gEditMenuHandler = this; +} + +void FSFloaterPoser::onFocusLost() +{ + if( LLEditMenuHandler::gEditMenuHandler == this ) + { + LLEditMenuHandler::gEditMenuHandler = nullptr; + } +} void FSFloaterPoser::enableVisualManipulators() { if (LLToolMgr::getInstance()->getCurrentToolset() != gCameraToolset) diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index cf75cd65c9..c3ab3d6f99 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -74,18 +74,23 @@ typedef enum E_Columns /// A class containing the UI fiddling for the Poser Floater. /// Please don't do LLJoint stuff here, fsposingmotion (the LLMotion derivative) is the class for that. /// -class FSFloaterPoser : public LLFloater +class FSFloaterPoser : public LLFloater, public LLEditMenuHandler { friend class LLFloaterReg; FSFloaterPoser(const LLSD &key); public: void updatePosedBones(); 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; } private: bool postBuild() override; void onOpen(const LLSD& key) override; void onClose(bool app_quitting) override; - + void onFocusReceived() override; + void onFocusLost() override; /// /// Refreshes the supplied pose list from the supplued subdirectory. /// diff --git a/indra/newview/fsmaniprotatejoint.cpp b/indra/newview/fsmaniprotatejoint.cpp index 6ab5724073..f1e177912c 100644 --- a/indra/newview/fsmaniprotatejoint.cpp +++ b/indra/newview/fsmaniprotatejoint.cpp @@ -932,14 +932,15 @@ bool FSManipRotateJoint::handleMouseDown(S32 x, S32 y, MASK mask) */ bool FSManipRotateJoint::handleMouseDownOnPart(S32 x, S32 y, MASK mask) { + auto * poser = dynamic_cast(LLFloaterReg::findInstance("fs_poser")); // Determine which ring (axis) is under the mouse, also highlights selectable joints. highlightManipulators(x, y); // For joint manipulation, require both a valid joint and avatar. - if (!mJoint || !mAvatar) + if (!mJoint || !mAvatar || !poser) { return false; } - + poser->setFocus(true); S32 hit_part = mHighlightedPart; // Save the joint’s current world rotation as the basis for the drag. 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 d9f809a8d7..cb50399b2b 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -937,7 +937,7 @@ width="430"> tool_tip="Toggle the visual manipulators on and off" height="50" width="50" - left="2" + left="5" bottom_delta="5" >