From f2962eb522ee5a26036542f5a9e8efa23a3d2788 Mon Sep 17 00:00:00 2001 From: William Weaver Date: Sun, 2 Mar 2025 20:51:14 +0300 Subject: [PATCH 01/23] fix: Correctly update shadows on RenderShadowResolutionScale change Shadows were not updating correctly when the RenderShadowResolutionScale setting was changed in-session, leading to broken shadows. This commit changes the signal listener for "RenderShadowResolutionScale" in llviewercontrol.cpp from `handleShadowsResized` to `handleSetShaderChanged`. `handleSetShaderChanged` ensures a full shader update, which is necessary for this setting to take effect immediately, similar to other render settings like RenderDeferredSSAO. This fix ensures shadows update correctly when the resolution scale is changed without requiring a viewer restart. --- indra/newview/llviewercontrol.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 8ee6d60c58..2d988bfa54 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -1286,7 +1286,10 @@ void settings_setup_listeners() setting_setup_signal_listener(gSavedSettings, "RenderSpecularResY", handleLUTBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderSpecularExponent", handleLUTBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderAnisotropic", handleAnisotropicChanged); - setting_setup_signal_listener(gSavedSettings, "RenderShadowResolutionScale", handleShadowsResized); + // Ensure shader update on shadow resolution scale change for correct shadow rendering. + // setting_setup_signal_listener(gSavedSettings, "RenderShadowResolutionScale", handleShadowsResized); + setting_setup_signal_listener(gSavedSettings, "RenderShadowResolutionScale", handleSetShaderChanged); + // setting_setup_signal_listener(gSavedSettings, "RenderGlow", handleReleaseGLBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderGlow", handleSetShaderChanged); setting_setup_signal_listener(gSavedSettings, "RenderGlowResolutionPow", handleReleaseGLBufferChanged); From b0a5216b272d24055735d73e1b51176a8635f3b3 Mon Sep 17 00:00:00 2001 From: William Weaver Date: Sun, 9 Mar 2025 17:51:45 +0300 Subject: [PATCH 02/23] Fixes: Improve SSAO Appearance Consistency for High-Resolution Snapshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detailed description of the changes. This commit addresses an issue where Screen Space Ambient Occlusion (SSAO) appearance was inconsistent, particularly in high-resolution snapshots compared to the main viewer window. Previously, the SSAO radii parameters (RenderSSAOScale and RenderSSAOMaxScale) were directly applied to the shader without considering the resolution difference between the viewer window and the render target (snapshot). This resulted in SSAO appearing too small and under-sampled in high-resolution snapshots, as the sampling radius was not scaled up to match the larger snapshot resolution. To resolve this, a scaling factor is now calculated based on the ratio of the viewer window's height to the snapshot's height: scale_factor = viewer_window_height / snapshot_height This scale factor is then used to divide the user-defined SSAO radius values before they are passed to the shader. When the snapshot resolution is higher than the viewer resolution, the `scale_factor` becomes less than 1. Dividing by this factor effectively increases the SSAO radii, ensuring the ambient occlusion effect scales proportionally to the higher resolution. For example, if the viewer is running at 1080p height and a snapshot is taken at 4096p height, the scale factor would be approximately 1080 / 4096 ≈ 0.2637. Dividing the SSAO radii by 0.2637 results in multiplying them by approximately 3.79, thus scaling up the SSAO effect to maintain visual consistency in the high-resolution snapshot. Intended Impact & Benefits: - **Visual Consistency:** Ensures that the SSAO effect maintains a consistent visual scale and intensity between the interactive viewer display and high-resolution snapshots, providing a more predictable and professional output. - **Improved User Experience:** Users capturing high-resolution snapshots will now experience a more accurate and visually pleasing SSAO effect, preventing the unintended diminishment of ambient occlusion and preserving the intended scene aesthetics. - **Balanced Quality Across Resolutions:** The fix dynamically adjusts the SSAO radii based on resolution, maintaining a balanced and consistent level of detail and intensity across varying output resolutions without requiring manual user adjustments. Testing: - **Build:** Rebuild the Aperture Viewer after applying this commit. - **Run:** Launch the viewer and ensure that Screen Space Ambient Occlusion (SSAO) is enabled in the graphics settings. - **Visual Comparison (Viewer vs. High-Resolution Snapshot):** 1. In the viewer, observe the appearance of SSAO in a scene with noticeable ambient occlusion effects. 2. Take a high-resolution snapshot to disk (e.g., 4096p or higher). 3. Compare the SSAO effect in the saved snapshot image to the SSAO in the live viewer window. The SSAO in the snapshot should now appear visually consistent in scale and intensity with the viewer, and not diminished or under-sampled. - **Resolution Variation (Recommended):** Test with different viewer window sizes (e.g., 720p, 1080p) and snapshot resolutions (including resolutions higher than the viewer) to verify that the SSAO consistency is maintained across a range of rendering targets. Documentation: - No specific updates to the Wiki are required as this commit is a bug fix that improves the existing SSAO feature's consistency. - It is recommended to include a note about this fix in the Viewer release notes to inform users about the improved SSAO behavior in high-resolution snapshots. --- indra/newview/pipeline.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index b9d9830298..8f86a1cda3 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -8796,14 +8796,20 @@ void LLPipeline::bindDeferredShader(LLGLSLShader& shader, LLRenderTarget* light_ shader.uniform1f(LLShaderMgr::DEFERRED_SHADOW_NOISE, RenderShadowNoise); shader.uniform1f(LLShaderMgr::DEFERRED_BLUR_SIZE, RenderShadowBlurSize); - shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_RADIUS, RenderSSAOScale); - shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_MAX_RADIUS, (GLfloat)RenderSSAOMaxScale); +// Compute scale factor to match AO appearance between view and snapshot. + F32 screen_to_target_scale_factor = (F32)gViewerWindow->getWindowHeightRaw() / deferred_target->getHeight(); + //shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_RADIUS, RenderSSAOScale); + shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_RADIUS, RenderSSAOScale / screen_to_target_scale_factor); + //shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_MAX_RADIUS, (GLfloat)RenderSSAOMaxScale); + shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_MAX_RADIUS, RenderSSAOMaxScale / screen_to_target_scale_factor); + // F32 ssao_factor = RenderSSAOFactor; shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_FACTOR, ssao_factor); shader.uniform1f(LLShaderMgr::DEFERRED_SSAO_FACTOR_INV, 1.0f/ssao_factor); LLVector3 ssao_effect = RenderSSAOEffect; + F32 matrix_diag = (ssao_effect[0] + 2.0f*ssao_effect[1])/3.0f; F32 matrix_nondiag = (ssao_effect[0] - ssao_effect[1])/3.0f; // This matrix scales (proj of color onto <1/rt(3),1/rt(3),1/rt(3)>) by From f9035a4b8b956be84acf794b3af39073de7f3674 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Wed, 27 Mar 2024 20:13:04 +0200 Subject: [PATCH 03/23] viewer#1061 Altitudes are invisible in region's environment --- .../skins/default/xui/en/panel_region_environment.xml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/indra/newview/skins/default/xui/en/panel_region_environment.xml b/indra/newview/skins/default/xui/en/panel_region_environment.xml index 0a7a597ce5..e13c6a03f1 100644 --- a/indra/newview/skins/default/xui/en/panel_region_environment.xml +++ b/indra/newview/skins/default/xui/en/panel_region_environment.xml @@ -272,8 +272,7 @@ top_pad="1" halign="right" name="txt_alt1"> - Sky [INDEX] - [ALTITUDE]m + Sky [INDEX] [ALTITUDE]m - Sky [INDEX] - [ALTITUDE]m + Sky [INDEX] [ALTITUDE]m - Sky [INDEX] - [ALTITUDE]m + Sky [INDEX] [ALTITUDE]m Date: Sun, 9 Mar 2025 23:22:28 +0000 Subject: [PATCH 04/23] 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 05/23] 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 06/23] 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 07/23] 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 08/23] 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 09/23] 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 10/23] 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 11/23] 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 12/23] 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" > Date: Thu, 13 Mar 2025 12:54:38 +0000 Subject: [PATCH 13/23] Revert "Merge pull request #88 from williamweaver/PRFS/Fixes-ShadowResolutionScale" This reverts commit f2b751419f3de54e7f9e7999bf0b3448f5c18841, reversing changes made to 4520e92f2153f05dc007ef0d4f4dd9cbda6e6aeb. While this does fix the problem, it ignores the reason why a deferred resize was previously employed. The correct fix is to fix the broken display() function. --- indra/newview/llviewercontrol.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 2d988bfa54..8ee6d60c58 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -1286,10 +1286,7 @@ void settings_setup_listeners() setting_setup_signal_listener(gSavedSettings, "RenderSpecularResY", handleLUTBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderSpecularExponent", handleLUTBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderAnisotropic", handleAnisotropicChanged); - // Ensure shader update on shadow resolution scale change for correct shadow rendering. - // setting_setup_signal_listener(gSavedSettings, "RenderShadowResolutionScale", handleShadowsResized); - setting_setup_signal_listener(gSavedSettings, "RenderShadowResolutionScale", handleSetShaderChanged); - // + setting_setup_signal_listener(gSavedSettings, "RenderShadowResolutionScale", handleShadowsResized); setting_setup_signal_listener(gSavedSettings, "RenderGlow", handleReleaseGLBufferChanged); setting_setup_signal_listener(gSavedSettings, "RenderGlow", handleSetShaderChanged); setting_setup_signal_listener(gSavedSettings, "RenderGlowResolutionPow", handleReleaseGLBufferChanged); From 520f2706ce24626ce61e9128ddb180d70dd658b0 Mon Sep 17 00:00:00 2001 From: Beq Date: Thu, 13 Mar 2025 13:10:46 +0000 Subject: [PATCH 14/23] let the deferred resize control when it is actually done. --- indra/newview/llviewerdisplay.cpp | 2 +- indra/newview/pipeline.cpp | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 3d762c5928..1cc89f21ca 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -475,7 +475,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) if (gResizeShadowTexture) { //skip render on frames where window has been resized gPipeline.resizeShadowTexture(); - gResizeShadowTexture = false; + // gResizeShadowTexture = false; // This prevents the deferred resize from working properly. } gSnapshot = for_snapshot; diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 8f86a1cda3..b29b7ec60c 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -789,6 +789,12 @@ void LLPipeline::requestResizeShadowTexture() void LLPipeline::resizeShadowTexture() { + // guard against zero size render target + if(mRT->width == 0 || mRT->height == 0) + { + return; + } + // releaseSunShadowTargets(); releaseSpotShadowTargets(); allocateShadowBuffer(mRT->width, mRT->height); @@ -8082,7 +8088,7 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst) { float frame_width = w; float frame_height = frame_width / snapshot_aspect; - // Centre this box in [0..1][0..1] + // Centre this box in [0..1]�[0..1] float y_offset = 0.5f * (h - frame_height); left = 0.f; top = y_offset / h; @@ -8093,7 +8099,7 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst) { float frame_height = h; float frame_width = h * snapshot_aspect; - // Centre this box in [0..1][0..1] + // Centre this box in [0..1]�[0..1] float x_offset = 0.5f * (w - frame_width); left = x_offset / w; top = 0.f; From 501100acef50b0c7764661232312a98058fd0d63 Mon Sep 17 00:00:00 2001 From: Beq Date: Thu, 13 Mar 2025 20:58:24 +0000 Subject: [PATCH 15/23] [FIRE-33200] Fix ShadowRes breaks shadows (update tagging) Original fix by William Weaver (paperwork) closes FIRE-33200 also: minor edit to remove stray unicode in comment --- indra/newview/pipeline.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index b29b7ec60c..cfbbfa42b8 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -789,7 +789,7 @@ void LLPipeline::requestResizeShadowTexture() void LLPipeline::resizeShadowTexture() { - // guard against zero size render target + // [FIRE-33200] changing shadowres requires reload - original fix by William Weaver (paperwork) if(mRT->width == 0 || mRT->height == 0) { return; @@ -8088,7 +8088,7 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst) { float frame_width = w; float frame_height = frame_width / snapshot_aspect; - // Centre this box in [0..1]�[0..1] + // Centre this box in [0..1]x[0..1] float y_offset = 0.5f * (h - frame_height); left = 0.f; top = y_offset / h; @@ -8099,7 +8099,7 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst) { float frame_height = h; float frame_width = h * snapshot_aspect; - // Centre this box in [0..1]�[0..1] + // Centre this box in [0..1]x[0..1] float x_offset = 0.5f * (w - frame_width); left = x_offset / w; top = 0.f; From 545b1dea5cadcc2429763d582538ab2f54a8f760 Mon Sep 17 00:00:00 2001 From: PanteraPolnocy Date: Fri, 14 Mar 2025 00:54:18 +0100 Subject: [PATCH 16/23] Updated Polish transaltion, minor fixes in English one (poser) --- .../skins/default/xui/en/floater_fs_poser.xml | 12 +-- .../skins/default/xui/pl/floater_fs_poser.xml | 80 ++++++++++--------- .../default/xui/pl/floater_phototools.xml | 6 +- 3 files changed, 50 insertions(+), 48 deletions(-) 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 cb50399b2b..81d0ba219f 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -884,7 +884,7 @@ width="430"> follows="left|top" height="21" layout="topleft" - label="Copy L > R" + label="Copy L > R" name="button_symmetrize_left_to_right" tool_tip="Double click to copy change from left side to right side." left="14" @@ -1459,7 +1459,7 @@ width="430"> follows="left|top" height="21" layout="topleft" - label="Copy R > L" + label="Copy R > L" name="button_symmetrize_right_to_left" tool_tip="Double click to copy change from right side to left side." left_pad="23" diff --git a/indra/newview/skins/default/xui/pl/floater_fs_poser.xml b/indra/newview/skins/default/xui/pl/floater_fs_poser.xml index a58b91988f..ca71054402 100644 --- a/indra/newview/skins/default/xui/pl/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/pl/floater_fs_poser.xml @@ -213,34 +213,59 @@ Czułość trackpada: + +