2566 lines
85 KiB
C++
2566 lines
85 KiB
C++
/**
|
|
* @file fsfloaterposer.cpp
|
|
* @brief View Model for posing your (and other) avatar(s).
|
|
*
|
|
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
|
|
* Phoenix Firestorm Viewer Source Code
|
|
* Copyright (c) 2024 Angeldark Raymaker @ 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
|
|
*
|
|
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
|
|
* $/LicenseInfo$
|
|
*/
|
|
|
|
#include "fsfloaterposer.h"
|
|
#include "fsposeranimator.h"
|
|
#include "fsvirtualtrackpad.h"
|
|
#include "llagent.h"
|
|
#include "llavatarnamecache.h"
|
|
#include "llcheckboxctrl.h"
|
|
#include "llcommonutils.h"
|
|
#include "llcontrolavatar.h"
|
|
#include "lldiriterator.h"
|
|
#include "llsdserialize.h"
|
|
#include "llscrolllistctrl.h"
|
|
#include "llsliderctrl.h"
|
|
#include "lltabcontainer.h"
|
|
#include "llviewercontrol.h"
|
|
#include "llviewerwindow.h"
|
|
#include "llwindow.h"
|
|
#include "llvoavatarself.h"
|
|
#include "llinventoryfunctions.h"
|
|
|
|
namespace
|
|
{
|
|
constexpr char POSE_INTERNAL_FORMAT_FILE_MASK[] = "*.xml";
|
|
constexpr char POSE_INTERNAL_FORMAT_FILE_EXT[] = ".xml";
|
|
constexpr char POSE_EXTERNAL_FORMAT_FILE_EXT[] = ".bvh";
|
|
constexpr char POSE_SAVE_SUBDIRECTORY[] = "poses";
|
|
constexpr std::string_view POSE_PRESETS_HANDS_SUBDIRECTORY = "hand_presets";
|
|
constexpr char XML_LIST_HEADER_STRING_PREFIX[] = "header_";
|
|
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";
|
|
constexpr std::string_view POSER_SAVEEXTERNALFORMAT_SAVE_KEY = "FSPoserSaveExternalFileAlso";
|
|
} // namespace
|
|
|
|
/// <summary>
|
|
/// The amount of deflection 'one unit' on the trackpad translates to in radians.
|
|
/// The trackpad ordinarily has a range of +1..-1; multiplied by PI, gives PI to -PI, or all 360 degrees of deflection.
|
|
/// </summary>
|
|
constexpr F32 NormalTrackpadRangeInRads = F_PI;
|
|
|
|
FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key)
|
|
{
|
|
// Bind requests, other controls are find-and-binds, see postBuild()
|
|
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.ToggleMirrorChanges", [this](LLUICtrl*, const LLSD&) { onToggleMirrorChange(); });
|
|
mCommitCallbackRegistrar.add("Poser.ToggleSympatheticChanges", [this](LLUICtrl*, const LLSD&) { onToggleSympatheticChange(); });
|
|
mCommitCallbackRegistrar.add("Poser.AdjustTrackPadSensitivity", [this](LLUICtrl*, const LLSD&) { onAdjustTrackpadSensitivity(); });
|
|
|
|
mCommitCallbackRegistrar.add("Poser.PositionSet", [this](LLUICtrl*, const LLSD&) { onAvatarPositionSet(); });
|
|
mCommitCallbackRegistrar.add("Poser.SetToTPose", [this](LLUICtrl*, const LLSD&) { onSetAvatarToTpose(); });
|
|
|
|
mCommitCallbackRegistrar.add("Poser.Advanced.PositionSet", [this](LLUICtrl*, const LLSD&) { onAdvancedPositionSet(); });
|
|
mCommitCallbackRegistrar.add("Poser.Advanced.ScaleSet", [this](LLUICtrl*, const LLSD&) { onAdvancedScaleSet(); });
|
|
mCommitCallbackRegistrar.add("Poser.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.Save", [this](LLUICtrl*, const LLSD&) { onClickPoseSave(); });
|
|
mCommitCallbackRegistrar.add("Pose.Menu", [this](LLUICtrl*, const LLSD& data) { onPoseMenuAction(data); });
|
|
mCommitCallbackRegistrar.add("Poser.BrowseCache", [this](LLUICtrl*, const LLSD&) { onClickBrowsePoseCache(); });
|
|
mCommitCallbackRegistrar.add("Poser.LoadLeftHand", [this](LLUICtrl*, const LLSD&) { onClickLoadLeftHandPose(); });
|
|
mCommitCallbackRegistrar.add("Poser.LoadRightHand", [this](LLUICtrl*, const LLSD&) { onClickLoadRightHandPose(); });
|
|
|
|
mCommitCallbackRegistrar.add("Poser.FlipPose", [this](LLUICtrl*, const LLSD&) { onClickFlipPose(); });
|
|
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); });
|
|
}
|
|
|
|
bool FSFloaterPoser::postBuild()
|
|
{
|
|
mAvatarTrackball = getChild<FSVirtualTrackpad>("limb_rotation");
|
|
mAvatarTrackball->setCommitCallback([this](LLUICtrl *, const LLSD &) { onLimbTrackballChanged(); });
|
|
|
|
mLimbYawSlider = getChild<LLSliderCtrl>("limb_yaw");
|
|
mLimbYawSlider->setCommitCallback([this](LLUICtrl *, const LLSD &) { onYawPitchRollSliderChanged(); });
|
|
|
|
mLimbPitchSlider = getChild<LLSliderCtrl>("limb_pitch");
|
|
mLimbPitchSlider->setCommitCallback([this](LLUICtrl *, const LLSD &) { onYawPitchRollSliderChanged(); });
|
|
|
|
mLimbRollSlider = getChild<LLSliderCtrl>("limb_roll");
|
|
mLimbRollSlider->setCommitCallback([this](LLUICtrl *, const LLSD &) { onYawPitchRollSliderChanged(); });
|
|
|
|
mJointsTabs = getChild<LLTabContainer>("joints_tabs");
|
|
mJointsTabs->setCommitCallback(
|
|
[this](LLUICtrl*, const LLSD&)
|
|
{
|
|
onJointTabSelect();
|
|
setRotationChangeButtons(false, false);
|
|
});
|
|
|
|
mAvatarSelectionScrollList = getChild<LLScrollListCtrl>("avatarSelection_scroll");
|
|
mAvatarSelectionScrollList->setCommitOnSelectionChange(true);
|
|
mAvatarSelectionScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onAvatarSelect(); });
|
|
|
|
mBodyJointsScrollList = getChild<LLScrollListCtrl>("body_joints_scroll");
|
|
mBodyJointsScrollList->setCommitOnSelectionChange(true);
|
|
mBodyJointsScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onJointTabSelect(); });
|
|
|
|
mFaceJointsScrollList = getChild<LLScrollListCtrl>("face_joints_scroll");
|
|
mFaceJointsScrollList->setCommitOnSelectionChange(true);
|
|
mFaceJointsScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onJointTabSelect(); });
|
|
|
|
mHandJointsScrollList = getChild<LLScrollListCtrl>("hand_joints_scroll");
|
|
mHandJointsScrollList->setCommitOnSelectionChange(true);
|
|
mHandJointsScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onJointTabSelect(); });
|
|
|
|
mMiscJointsScrollList = getChild<LLScrollListCtrl>("misc_joints_scroll");
|
|
mMiscJointsScrollList->setCommitOnSelectionChange(true);
|
|
mMiscJointsScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onJointTabSelect(); });
|
|
|
|
mCollisionVolumesScrollList = getChild<LLScrollListCtrl>("collision_volumes_scroll");
|
|
mCollisionVolumesScrollList->setCommitOnSelectionChange(true);
|
|
mCollisionVolumesScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onJointTabSelect(); });
|
|
|
|
mEntireAvJointScroll = getChild<LLScrollListCtrl>("entireAv_joint_scroll");
|
|
|
|
mPosesScrollList = getChild<LLScrollListCtrl>("poses_scroll");
|
|
mPosesScrollList->setCommitOnSelectionChange(true);
|
|
mPosesScrollList->setCommitCallback([this](LLUICtrl *, const LLSD &) { onPoseFileSelect(); });
|
|
|
|
mToggleAdvancedPanelBtn = getChild<LLButton>("toggleAdvancedPanel");
|
|
if (gSavedSettings.getBOOL(POSER_ADVANCEDWINDOWSTATE_SAVE_KEY))
|
|
mToggleAdvancedPanelBtn->setValue(true);
|
|
|
|
mTrackpadSensitivitySlider = getChild<LLSliderCtrl>("trackpad_sensitivity_slider");
|
|
mTrackpadSensitivitySlider->setValue(gSavedSettings.getF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY));
|
|
|
|
mPoseSaveNameEditor = getChild<LLLineEditor>("pose_save_name");
|
|
mPoseSaveNameEditor->setPrevalidate(&LLTextValidate::validateASCIIPrintableNoPipe);
|
|
|
|
mHandPresetsScrollList = getChild<LLScrollListCtrl>("hand_presets_scroll");
|
|
|
|
mPosXSlider = getChild<LLSliderCtrl>("av_position_inout");
|
|
mPosYSlider = getChild<LLSliderCtrl>("av_position_leftright");
|
|
mPosZSlider = getChild<LLSliderCtrl>("av_position_updown");
|
|
|
|
mAdvPosXSlider = getChild<LLSliderCtrl>("Advanced_Position_X");
|
|
mAdvPosYSlider = getChild<LLSliderCtrl>("Advanced_Position_Y");
|
|
mAdvPosZSlider = getChild<LLSliderCtrl>("Advanced_Position_Z");
|
|
|
|
mAdvScaleXSlider = getChild<LLSliderCtrl>("Advanced_Scale_X");
|
|
mAdvScaleYSlider = getChild<LLSliderCtrl>("Advanced_Scale_Y");
|
|
mAdvScaleZSlider = getChild<LLSliderCtrl>("Advanced_Scale_Z");
|
|
|
|
mPosesLoadSavePnl = getChild<LLPanel>("poses_loadSave");
|
|
mStartStopPosingBtn = getChild<LLButton>("start_stop_posing_button");
|
|
mToggleLoadSavePanelBtn = getChild<LLButton>("toggleLoadSavePanel");
|
|
mBrowserFolderBtn = getChild<LLButton>("open_poseDir_button");
|
|
mLoadPosesBtn = getChild<LLButton>("load_poses_button");
|
|
mSavePosesBtn = getChild<LLButton>("save_poses_button");
|
|
|
|
mFlipPoseBtn = getChild<LLButton>("FlipPose_avatar");
|
|
mFlipJointBtn = getChild<LLButton>("FlipJoint_avatar");
|
|
mRecaptureBtn = getChild<LLButton>("button_RecaptureParts");
|
|
mTogglePosingBonesBtn = getChild<LLButton>("toggle_PosingSelectedBones");
|
|
|
|
mToggleMirrorRotationBtn = getChild<LLButton>("button_toggleMirrorRotation");
|
|
mToggleSympatheticRotationBtn = getChild<LLButton>("button_toggleSympatheticRotation");
|
|
mToggleDeltaModeBtn = getChild<LLButton>("delta_mode_toggle");
|
|
mRedoChangeBtn = getChild<LLButton>("button_redo_change");
|
|
mSetToTposeButton = getChild<LLButton>("set_t_pose_button");
|
|
|
|
mJointsParentPnl = getChild<LLPanel>("joints_parent_panel");
|
|
mAdvancedParentPnl = getChild<LLPanel>("advanced_parent_panel");
|
|
mTrackballPnl = getChild<LLPanel>("trackball_panel");
|
|
mPositionRotationPnl = getChild<LLPanel>("positionRotation_panel");
|
|
mBodyJointsPnl = getChild<LLPanel>("body_joints_panel");
|
|
mFaceJointsPnl = getChild<LLPanel>("face_joints_panel");
|
|
mHandsTabs = getChild<LLTabContainer>("hands_tabs");
|
|
mHandsJointsPnl = mHandsTabs->getChild<LLPanel>("hands_joints_panel");
|
|
mMiscJointsPnl = getChild<LLPanel>("misc_joints_panel");
|
|
mCollisionVolumesPnl = getChild<LLPanel>("collision_volumes_panel");
|
|
|
|
mAlsoSaveBvhCbx = getChild<LLCheckBoxCtrl>("also_save_bvh_checkbox");
|
|
mResetBaseRotCbx = getChild<LLCheckBoxCtrl>("reset_base_rotation_on_edit_checkbox");
|
|
mResetBaseRotCbx->setCommitCallback([this](LLUICtrl*, const LLSD&) { onClickSetBaseRotZero(); });
|
|
|
|
mTrackpadSensitivitySpnr = getChild<LLUICtrl>("trackpad_sensitivity_spinner");
|
|
mYawSpnr = getChild<LLUICtrl>("limb_yaw_spinner");
|
|
mPitchSpnr = getChild<LLUICtrl>("limb_pitch_spinner");
|
|
mRollSpnr = getChild<LLUICtrl>("limb_roll_spinner");
|
|
mUpDownSpnr = getChild<LLUICtrl>("av_position_updown_spinner");
|
|
mLeftRightSpnr = getChild<LLUICtrl>("av_position_leftright_spinner");
|
|
mInOutSpnr = getChild<LLUICtrl>("av_position_inout_spinner");
|
|
mAdvPosXSpnr = getChild<LLUICtrl>("adv_posx_spinner");
|
|
mAdvPosYSpnr = getChild<LLUICtrl>("adv_posy_spinner");
|
|
mAdvPosZSpnr = getChild<LLUICtrl>("adv_posz_spinner");
|
|
mScaleXSpnr = getChild<LLUICtrl>("adv_scalex_spinner");
|
|
mScaleYSpnr = getChild<LLUICtrl>("adv_scaley_spinner");
|
|
mScaleZSpnr = getChild<LLUICtrl>("adv_scalez_spinner");
|
|
|
|
return true;
|
|
}
|
|
|
|
void FSFloaterPoser::onOpen(const LLSD& key)
|
|
{
|
|
createUserPoseDirectoryIfNeeded();
|
|
onAvatarsRefresh();
|
|
refreshJointScrollListMembers();
|
|
onJointTabSelect();
|
|
onOpenSetAdvancedPanel();
|
|
refreshPoseScroll(mHandPresetsScrollList, POSE_PRESETS_HANDS_SUBDIRECTORY);
|
|
startPosingSelf();
|
|
|
|
LLFloater::onOpen(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();
|
|
|
|
LLFloater::onClose(app_quitting);
|
|
}
|
|
|
|
void FSFloaterPoser::refreshPoseScroll(LLScrollListCtrl* posesScrollList, std::optional<std::string_view> subDirectory)
|
|
{
|
|
if (!posesScrollList)
|
|
return;
|
|
|
|
posesScrollList->clearRows();
|
|
|
|
std::string dir = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY);
|
|
if (subDirectory.has_value())
|
|
gDirUtilp->append(dir, std::string(subDirectory.value()));
|
|
|
|
std::string file;
|
|
LLDirIterator dir_iter(dir, POSE_INTERNAL_FORMAT_FILE_MASK);
|
|
while (dir_iter.next(file))
|
|
{
|
|
std::string path = gDirUtilp->add(dir, file);
|
|
std::string name = gDirUtilp->getBaseFileName(LLURI::unescape(path), true);
|
|
|
|
LLSD row;
|
|
row["columns"][0]["column"] = "name";
|
|
row["columns"][0]["value"] = name;
|
|
|
|
llifstream infile;
|
|
infile.open(path);
|
|
if (!infile.is_open())
|
|
{
|
|
LL_WARNS("Posing") << "Skipping: Cannot read file in: " << path << LL_ENDL;
|
|
continue;
|
|
}
|
|
|
|
LLSD data;
|
|
if (LLSDParser::PARSE_FAILURE == LLSDSerialize::fromXML(data, infile))
|
|
{
|
|
LL_WARNS("Posing") << "Skipping: Failed to parse pose file: " << path << LL_ENDL;
|
|
continue;
|
|
}
|
|
|
|
posesScrollList->addElement(row);
|
|
}
|
|
}
|
|
|
|
void FSFloaterPoser::onPoseFileSelect()
|
|
{
|
|
LLScrollListItem* item = mPosesScrollList->getFirstSelected();
|
|
if (!item)
|
|
return;
|
|
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
bool enableButtons = mPoserAnimator.isPosingAvatar(avatar);
|
|
mLoadPosesBtn->setEnabled(enableButtons);
|
|
mSavePosesBtn->setEnabled(enableButtons);
|
|
|
|
std::string poseName = item->getColumn(0)->getValue().asString();
|
|
if (poseName.empty())
|
|
return;
|
|
|
|
LLStringExplicit name = LLStringExplicit(poseName);
|
|
mPoseSaveNameEditor->setEnabled(enableButtons);
|
|
mPoseSaveNameEditor->setText(name);
|
|
|
|
bool isDeltaSave = !poseFileStartsFromTeePose(name);
|
|
if (isDeltaSave)
|
|
mLoadPosesBtn->setLabel("Load Diff");
|
|
else
|
|
mLoadPosesBtn->setLabel("Load Pose");
|
|
}
|
|
|
|
void FSFloaterPoser::onClickPoseSave()
|
|
{
|
|
std::string filename = mPoseSaveNameEditor->getValue().asString();
|
|
if (filename.empty())
|
|
return;
|
|
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
bool successfulSave = savePoseToXml(avatar, filename);
|
|
if (successfulSave)
|
|
{
|
|
refreshPoseScroll(mPosesScrollList);
|
|
setUiSelectedAvatarSaveFileName(filename);
|
|
|
|
if (getSavingToBvh())
|
|
savePoseToBvh(avatar, filename);
|
|
|
|
// TODO: provide feedback for save
|
|
}
|
|
}
|
|
|
|
void FSFloaterPoser::createUserPoseDirectoryIfNeeded()
|
|
{
|
|
std::string userPath =
|
|
gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY);
|
|
|
|
std::string userHandPresetsPath = userPath + gDirUtilp->getDirDelimiter() + std::string(POSE_PRESETS_HANDS_SUBDIRECTORY);
|
|
if (gDirUtilp->fileExists(userHandPresetsPath))
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (!gDirUtilp->fileExists(userPath))
|
|
{
|
|
LL_WARNS("Poser") << "Couldn't find folder: " << userPath << " - creating one." << LL_ENDL;
|
|
LLFile::mkdir(userPath);
|
|
}
|
|
|
|
if (!gDirUtilp->fileExists(userHandPresetsPath))
|
|
{
|
|
LL_WARNS("Poser") << "Couldn't find folder: " << userHandPresetsPath << " - creating one." << LL_ENDL;
|
|
LLFile::mkdir(userHandPresetsPath);
|
|
}
|
|
|
|
std::string sourcePresetPath =
|
|
gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, POSE_SAVE_SUBDIRECTORY, std::string(POSE_PRESETS_HANDS_SUBDIRECTORY));
|
|
|
|
if (!gDirUtilp->fileExists(sourcePresetPath))
|
|
return;
|
|
|
|
auto posesToCopy = gDirUtilp->getFilesInDir(sourcePresetPath);
|
|
for (auto pose : posesToCopy)
|
|
{
|
|
std::string source = sourcePresetPath + gDirUtilp->getDirDelimiter() + pose;
|
|
std::string destination = userHandPresetsPath + gDirUtilp->getDirDelimiter() + pose;
|
|
|
|
if (!LLFile::copy(source, destination))
|
|
LL_WARNS("Poser") << "Failed to copy " << source << " to " << destination << LL_ENDL;
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
LL_WARNS("Posing") << "Exception caught trying to create: " << userPath << e.what() << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFileName)
|
|
{
|
|
if (poseFileName.empty())
|
|
return false;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return false;
|
|
|
|
createUserPoseDirectoryIfNeeded();
|
|
|
|
try
|
|
{
|
|
bool savingDiff = !mPoserAnimator.allBaseRotationsAreZero(avatar);
|
|
LLSD record;
|
|
record["version"]["value"] = (S32)5;
|
|
record["startFromTeePose"]["value"] = !savingDiff;
|
|
|
|
LLVector3 rotation, position, scale, zeroVector;
|
|
bool baseRotationIsZero;
|
|
|
|
for (const FSPoserAnimator::FSPoserJoint& pj : mPoserAnimator.PoserJoints)
|
|
{
|
|
std::string bone_name = pj.jointName();
|
|
bool posingThisJoint = mPoserAnimator.isPosingAvatarJoint(avatar, pj);
|
|
|
|
record[bone_name] = bone_name;
|
|
record[bone_name]["enabled"] = posingThisJoint;
|
|
if (!posingThisJoint)
|
|
continue;
|
|
|
|
if (!mPoserAnimator.tryGetJointSaveVectors(avatar, pj, &rotation, &position, &scale, &baseRotationIsZero))
|
|
continue;
|
|
|
|
bool jointRotPosScaleAllZero = rotation == zeroVector && position == zeroVector && scale == zeroVector;
|
|
|
|
if (savingDiff && jointRotPosScaleAllZero)
|
|
continue;
|
|
|
|
record[bone_name]["jointBaseRotationIsZero"] = baseRotationIsZero;
|
|
record[bone_name]["rotation"] = rotation.getValue();
|
|
record[bone_name]["position"] = position.getValue();
|
|
record[bone_name]["scale"] = scale.getValue();
|
|
}
|
|
|
|
std::string fullSavePath =
|
|
gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, poseFileName + POSE_INTERNAL_FORMAT_FILE_EXT);
|
|
|
|
llofstream file;
|
|
file.open(fullSavePath.c_str());
|
|
if (!file.is_open())
|
|
{
|
|
LL_WARNS("Poser") << "Unable to save pose!" << LL_ENDL;
|
|
return false;
|
|
}
|
|
LLSDSerialize::toPrettyXML(record, file);
|
|
file.close();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
LL_WARNS("Posing") << "Exception caught in saveToXml: " << e.what() << LL_ENDL;
|
|
return false;
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
void FSFloaterPoser::onClickToggleSelectedBoneEnabled()
|
|
{
|
|
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);
|
|
mPoserAnimator.setPosingAvatarJoint(avatar, *item, !currentlyPosing);
|
|
}
|
|
|
|
refreshRotationSliders();
|
|
refreshTrackpadCursor();
|
|
refreshTextHighlightingOnJointScrollLists();
|
|
}
|
|
|
|
void FSFloaterPoser::onClickFlipSelectedJoints()
|
|
{
|
|
auto selectedJoints = getUiSelectedPoserJoints();
|
|
if (selectedJoints.size() < 1)
|
|
return;
|
|
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
for (auto item : selectedJoints)
|
|
{
|
|
// need to be posing the joint to flip
|
|
bool currentlyPosingJoint = mPoserAnimator.isPosingAvatarJoint(avatar, *item);
|
|
if (!currentlyPosingJoint)
|
|
continue;
|
|
|
|
auto oppositeJoint = mPoserAnimator.getPoserJointByName(item->mirrorJointName());
|
|
if (oppositeJoint)
|
|
{
|
|
bool currentlyPosingOppositeJoint = mPoserAnimator.isPosingAvatarJoint(avatar, *oppositeJoint);
|
|
if (!currentlyPosingOppositeJoint)
|
|
continue;
|
|
}
|
|
|
|
// If one selects a joint and its opposite, this would otherwise flip each of them sequentially to yeild no net result.
|
|
// This allows the user to select a joint and its opposite, or only select one joint, and get the same result.
|
|
if (std::find(selectedJoints.begin(), selectedJoints.end(), oppositeJoint) != selectedJoints.end())
|
|
{
|
|
if (!item->dontFlipOnMirror())
|
|
continue;
|
|
}
|
|
|
|
mPoserAnimator.reflectJoint(avatar, item);
|
|
}
|
|
|
|
refreshRotationSliders();
|
|
refreshTrackpadCursor();
|
|
}
|
|
|
|
void FSFloaterPoser::onClickFlipPose()
|
|
{
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
mPoserAnimator.flipEntirePose(avatar);
|
|
|
|
refreshRotationSliders();
|
|
refreshTrackpadCursor();
|
|
}
|
|
|
|
void FSFloaterPoser::onClickRecaptureSelectedBones()
|
|
{
|
|
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.recaptureJoint(avatar, *item, getJointTranslation(item->jointName()), getJointNegation(item->jointName()));
|
|
}
|
|
|
|
setSavePosesButtonText(true);
|
|
refreshRotationSliders();
|
|
refreshTrackpadCursor();
|
|
refreshTextHighlightingOnJointScrollLists();
|
|
}
|
|
|
|
void FSFloaterPoser::onClickBrowsePoseCache()
|
|
{
|
|
createUserPoseDirectoryIfNeeded();
|
|
|
|
std::string pathname = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY);
|
|
gViewerWindow->getWindow()->openFile(pathname);
|
|
}
|
|
|
|
void FSFloaterPoser::onCommitSpinner(LLUICtrl* spinner)
|
|
{
|
|
if (!spinner)
|
|
return;
|
|
|
|
F32 value = (F32)spinner->getValue().asReal();
|
|
|
|
if (spinner == mTrackpadSensitivitySpnr)
|
|
{
|
|
mTrackpadSensitivitySlider->setValue(value);
|
|
onAdjustTrackpadSensitivity();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mInOutSpnr || spinner == mAdvPosXSpnr)
|
|
{
|
|
mPosXSlider->setValue(value);
|
|
onAvatarPositionSet();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mLeftRightSpnr || spinner == mAdvPosYSpnr)
|
|
{
|
|
mPosYSlider->setValue(value);
|
|
onAvatarPositionSet();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mUpDownSpnr || spinner == mAdvPosZSpnr)
|
|
{
|
|
mPosZSlider->setValue(value);
|
|
onAvatarPositionSet();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mScaleXSpnr)
|
|
{
|
|
mAdvScaleXSlider->setValue(value);
|
|
onAdvancedScaleSet();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mScaleYSpnr)
|
|
{
|
|
mAdvScaleYSlider->setValue(value);
|
|
onAdvancedScaleSet();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mScaleZSpnr)
|
|
{
|
|
mAdvScaleZSlider->setValue(value);
|
|
onAdvancedScaleSet();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mYawSpnr)
|
|
{
|
|
mLimbYawSlider->setValue(value);
|
|
onYawPitchRollSliderChanged();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mPitchSpnr)
|
|
{
|
|
mLimbPitchSlider->setValue(value);
|
|
onYawPitchRollSliderChanged();
|
|
return;
|
|
}
|
|
|
|
if (spinner == mRollSpnr)
|
|
{
|
|
mLimbRollSlider->setValue(value);
|
|
onYawPitchRollSliderChanged();
|
|
return;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
refreshRotationSliders();
|
|
refreshTrackpadCursor();
|
|
refreshAvatarPositionSliders();
|
|
}
|
|
|
|
void FSFloaterPoser::onPoseMenuAction(const LLSD& param)
|
|
{
|
|
std::string loadStyle = param.asString();
|
|
if (loadStyle.empty())
|
|
return;
|
|
|
|
LLScrollListItem* item = mPosesScrollList->getFirstSelected();
|
|
if (!item)
|
|
return;
|
|
|
|
std::string poseName = item->getColumn(0)->getValue().asString();
|
|
|
|
E_LoadPoseMethods loadType = ROT_POS_AND_SCALES; // the default is to load everything
|
|
if (loadStyle == "rotation")
|
|
loadType = ROTATIONS;
|
|
else if (loadStyle == "position")
|
|
loadType = POSITIONS;
|
|
else if (loadStyle == "scale")
|
|
loadType = SCALES;
|
|
else if (loadStyle == "rot_pos")
|
|
loadType = ROTATIONS_AND_POSITIONS;
|
|
else if (loadStyle == "rot_scale")
|
|
loadType = ROTATIONS_AND_SCALES;
|
|
else if (loadStyle == "pos_scale")
|
|
loadType = POSITIONS_AND_SCALES;
|
|
else if (loadStyle == "all")
|
|
loadType = ROT_POS_AND_SCALES;
|
|
else if (loadStyle == "selective")
|
|
loadType = SELECTIVE;
|
|
else if (loadStyle == "selective_rot")
|
|
loadType = SELECTIVE_ROT;
|
|
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
loadPoseFromXml(avatar, poseName, loadType);
|
|
onJointTabSelect();
|
|
refreshJointScrollListMembers();
|
|
setSavePosesButtonText(mPoserAnimator.allBaseRotationsAreZero(avatar));
|
|
}
|
|
|
|
bool FSFloaterPoser::notDoubleClicked()
|
|
{
|
|
auto timeIntervalSinceLastExecution = std::chrono::system_clock::now() - mTimeLastExecutedDoubleClickMethod;
|
|
mTimeLastExecutedDoubleClickMethod = std::chrono::system_clock::now();
|
|
|
|
return timeIntervalSinceLastExecution > mDoubleClickInterval;
|
|
}
|
|
|
|
void FSFloaterPoser::onClickLoadLeftHandPose()
|
|
{
|
|
if (notDoubleClicked())
|
|
return;
|
|
|
|
onClickLoadHandPose(false);
|
|
}
|
|
|
|
void FSFloaterPoser::onClickLoadRightHandPose()
|
|
{
|
|
if (notDoubleClicked())
|
|
return;
|
|
|
|
onClickLoadHandPose(true);
|
|
}
|
|
|
|
void FSFloaterPoser::onClickLoadHandPose(bool isRightHand)
|
|
{
|
|
LLScrollListItem* item = mHandPresetsScrollList->getFirstSelected();
|
|
if (!item)
|
|
return;
|
|
|
|
std::string poseName = item->getColumn(0)->getValue().asString();
|
|
std::string pathname = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, std::string(POSE_PRESETS_HANDS_SUBDIRECTORY));
|
|
if (!gDirUtilp->fileExists(pathname))
|
|
return;
|
|
|
|
std::string fullPath =
|
|
gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, std::string(POSE_PRESETS_HANDS_SUBDIRECTORY), poseName + POSE_INTERNAL_FORMAT_FILE_EXT);
|
|
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
try
|
|
{
|
|
LLSD pose;
|
|
llifstream infile;
|
|
LLVector3 vec3;
|
|
|
|
infile.open(fullPath);
|
|
if (!infile.is_open())
|
|
return;
|
|
|
|
while (!infile.eof())
|
|
{
|
|
S32 lineCount = LLSDSerialize::fromXML(pose, infile);
|
|
if (lineCount == LLSDParser::PARSE_FAILURE)
|
|
{
|
|
LL_WARNS("Posing") << "Failed to parse loading a file for a hand: " << poseName << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
for (LLSD::map_const_iterator itr = pose.beginMap(); itr != pose.endMap(); ++itr)
|
|
{
|
|
std::string const& name = itr->first;
|
|
LLSD const& control_map = itr->second;
|
|
|
|
if (name.find("Hand") == std::string::npos)
|
|
continue;
|
|
if (isRightHand != (name.find("Left") == std::string::npos))
|
|
continue;
|
|
|
|
const FSPoserAnimator::FSPoserJoint* poserJoint = mPoserAnimator.getPoserJointByName(name);
|
|
if (!poserJoint)
|
|
continue;
|
|
|
|
vec3.setValue(control_map["rotation"]);
|
|
mPoserAnimator.loadJointRotation(avatar, poserJoint, true, vec3);
|
|
}
|
|
}
|
|
}
|
|
catch ( const std::exception& e )
|
|
{
|
|
LL_WARNS("Posing") << "Threw an exception trying to load a hand pose: " << poseName << " exception: " << e.what() << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
bool FSFloaterPoser::poseFileStartsFromTeePose(const std::string& poseFileName)
|
|
{
|
|
std::string pathname = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY);
|
|
if (!gDirUtilp->fileExists(pathname))
|
|
return false;
|
|
|
|
std::string fullPath =
|
|
gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, poseFileName + POSE_INTERNAL_FORMAT_FILE_EXT);
|
|
|
|
try
|
|
{
|
|
LLSD pose;
|
|
llifstream infile;
|
|
bool startFromZeroRot = false;
|
|
|
|
infile.open(fullPath);
|
|
if (!infile.is_open())
|
|
return false;
|
|
|
|
S32 lineCount = LLSDSerialize::fromXML(pose, infile);
|
|
if (lineCount == LLSDParser::PARSE_FAILURE)
|
|
return startFromZeroRot;
|
|
|
|
for (LLSD::map_const_iterator itr = pose.beginMap(); itr != pose.endMap(); ++itr)
|
|
{
|
|
std::string const& name = itr->first;
|
|
LLSD const& control_map = itr->second;
|
|
|
|
if (name == "startFromTeePose")
|
|
startFromZeroRot = control_map["value"].asBoolean();
|
|
}
|
|
|
|
return startFromZeroRot;
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
LL_WARNS("Posing") << "Unable to load or parse the pose: " << poseFileName << " exception: " << e.what() << LL_ENDL;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& poseFileName, E_LoadPoseMethods loadMethod)
|
|
{
|
|
std::string pathname = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY);
|
|
if (!gDirUtilp->fileExists(pathname))
|
|
return;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
std::string fullPath =
|
|
gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, poseFileName + POSE_INTERNAL_FORMAT_FILE_EXT);
|
|
|
|
bool loadRotations = loadMethod == ROTATIONS || loadMethod == ROTATIONS_AND_POSITIONS || loadMethod == ROTATIONS_AND_SCALES ||
|
|
loadMethod == ROT_POS_AND_SCALES || loadMethod == SELECTIVE || loadMethod == SELECTIVE_ROT;
|
|
bool loadPositions = loadMethod == POSITIONS || loadMethod == ROTATIONS_AND_POSITIONS || loadMethod == POSITIONS_AND_SCALES ||
|
|
loadMethod == ROT_POS_AND_SCALES || loadMethod == SELECTIVE;
|
|
bool loadScales = loadMethod == SCALES || loadMethod == POSITIONS_AND_SCALES || loadMethod == ROTATIONS_AND_SCALES ||
|
|
loadMethod == ROT_POS_AND_SCALES || loadMethod == SELECTIVE;
|
|
bool loadSelective = loadMethod == SELECTIVE || loadMethod == SELECTIVE_ROT;
|
|
|
|
try
|
|
{
|
|
LLSD pose;
|
|
llifstream infile;
|
|
LLVector3 vec3;
|
|
LLQuaternion quat;
|
|
bool enabled;
|
|
bool setJointBaseRotationToZero;
|
|
S32 version = 0;
|
|
bool startFromZeroRot = true;
|
|
|
|
infile.open(fullPath);
|
|
if (!infile.is_open())
|
|
return;
|
|
|
|
while (!infile.eof())
|
|
{
|
|
S32 lineCount = LLSDSerialize::fromXML(pose, infile);
|
|
if (lineCount == LLSDParser::PARSE_FAILURE)
|
|
{
|
|
LL_WARNS("Posing") << "Failed to parse file: " << poseFileName << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
for (LLSD::map_const_iterator itr = pose.beginMap(); itr != pose.endMap(); ++itr)
|
|
{
|
|
std::string const& name = itr->first;
|
|
LLSD const& control_map = itr->second;
|
|
|
|
if (name == "startFromTeePose")
|
|
startFromZeroRot = control_map["value"].asBoolean();
|
|
|
|
if (name == "version")
|
|
version = (S32)control_map["value"].asInteger();
|
|
}
|
|
|
|
bool loadPositionsAndScalesAsDeltas = false;
|
|
if (version > 3)
|
|
loadPositionsAndScalesAsDeltas = true;
|
|
|
|
for (LLSD::map_const_iterator itr = pose.beginMap(); itr != pose.endMap(); ++itr)
|
|
{
|
|
std::string const& name = itr->first;
|
|
LLSD const& control_map = itr->second;
|
|
|
|
const FSPoserAnimator::FSPoserJoint *poserJoint = mPoserAnimator.getPoserJointByName(name);
|
|
if (!poserJoint)
|
|
continue;
|
|
|
|
if (loadSelective && mPoserAnimator.isPosingAvatarJoint(avatar, *poserJoint))
|
|
continue;
|
|
|
|
if (control_map.has("enabled"))
|
|
{
|
|
enabled = control_map["enabled"].asBoolean();
|
|
mPoserAnimator.setPosingAvatarJoint(avatar, *poserJoint, enabled || loadSelective);
|
|
}
|
|
|
|
if (control_map.has("jointBaseRotationIsZero"))
|
|
setJointBaseRotationToZero = control_map["jointBaseRotationIsZero"].asBoolean();
|
|
else
|
|
setJointBaseRotationToZero = startFromZeroRot;
|
|
|
|
if (loadPositions && control_map.has("position"))
|
|
{
|
|
vec3.setValue(control_map["position"]);
|
|
mPoserAnimator.loadJointPosition(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3);
|
|
}
|
|
|
|
if (loadRotations && control_map.has("rotation"))
|
|
{
|
|
vec3.setValue(control_map["rotation"]);
|
|
mPoserAnimator.loadJointRotation(avatar, poserJoint, setJointBaseRotationToZero, vec3);
|
|
}
|
|
|
|
if (loadScales && control_map.has("scale"))
|
|
{
|
|
vec3.setValue(control_map["scale"]);
|
|
mPoserAnimator.loadJointScale(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch ( const std::exception & e )
|
|
{
|
|
LL_WARNS("Posing") << "Everything caught fire trying to load the pose: " << poseFileName << " exception: " << e.what() << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
void FSFloaterPoser::startPosingSelf()
|
|
{
|
|
if (!gAgentAvatarp || gAgentAvatarp.isNull())
|
|
return;
|
|
|
|
setUiSelectedAvatar(gAgentAvatarp->getID());
|
|
LLVOAvatar* avatar = getAvatarByUuid(gAgentAvatarp->getID());
|
|
if (!avatar)
|
|
return;
|
|
|
|
bool arePosingSelected = mPoserAnimator.isPosingAvatar(avatar);
|
|
if (!arePosingSelected && couldAnimateAvatar(avatar))
|
|
mPoserAnimator.tryPosingAvatar(avatar);
|
|
|
|
onAvatarSelect();
|
|
}
|
|
|
|
void FSFloaterPoser::stopPosingAllAvatars()
|
|
{
|
|
if (!gAgentAvatarp || gAgentAvatarp.isNull())
|
|
return;
|
|
|
|
for (auto listItem : mAvatarSelectionScrollList->getAllData())
|
|
{
|
|
LLScrollListCell* cell = listItem->getColumn(COL_UUID);
|
|
if (!cell)
|
|
continue;
|
|
|
|
LLUUID selectedAvatarId = cell->getValue().asUUID();
|
|
LLVOAvatar* listAvatar = getAvatarByUuid(selectedAvatarId);
|
|
|
|
if (mPoserAnimator.isPosingAvatar(listAvatar))
|
|
mPoserAnimator.stopPosingAvatar(listAvatar);
|
|
}
|
|
|
|
onAvatarSelect();
|
|
}
|
|
|
|
void FSFloaterPoser::onPoseStartStop()
|
|
{
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
bool arePosingSelected = mPoserAnimator.isPosingAvatar(avatar);
|
|
if (arePosingSelected)
|
|
{
|
|
mPoserAnimator.stopPosingAvatar(avatar);
|
|
}
|
|
else
|
|
{
|
|
if (!couldAnimateAvatar(avatar))
|
|
return;
|
|
|
|
if (!havePermissionToAnimateAvatar(avatar))
|
|
return;
|
|
|
|
mPoserAnimator.tryPosingAvatar(avatar);
|
|
}
|
|
|
|
onAvatarsRefresh();
|
|
onAvatarSelect();
|
|
}
|
|
|
|
bool FSFloaterPoser::couldAnimateAvatar(LLVOAvatar *avatar) const
|
|
{
|
|
if (!avatar || avatar->isDead())
|
|
return false;
|
|
if (avatar->getRegion() != gAgent.getRegion())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FSFloaterPoser::havePermissionToAnimateAvatar(LLVOAvatar *avatar) const
|
|
{
|
|
if (!avatar || avatar->isDead())
|
|
return false;
|
|
if (avatar->isSelf())
|
|
return true;
|
|
if (avatar->isControlAvatar())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void FSFloaterPoser::poseControlsEnable(bool enable)
|
|
{
|
|
mAdvancedParentPnl->setEnabled(enable);
|
|
mTrackballPnl->setEnabled(enable);
|
|
mFlipPoseBtn->setEnabled(enable);
|
|
mFlipJointBtn->setEnabled(enable);
|
|
mRecaptureBtn->setEnabled(enable);
|
|
mTogglePosingBonesBtn->setEnabled(enable);
|
|
mLoadPosesBtn->setEnabled(enable);
|
|
mSavePosesBtn->setEnabled(enable);
|
|
mPoseSaveNameEditor->setEnabled(enable);
|
|
}
|
|
|
|
void FSFloaterPoser::refreshJointScrollListMembers()
|
|
{
|
|
mEntireAvJointScroll->clearRows();
|
|
mBodyJointsScrollList->clearRows();
|
|
mFaceJointsScrollList->clearRows();
|
|
mHandJointsScrollList->clearRows();
|
|
mMiscJointsScrollList->clearRows();
|
|
mCollisionVolumesScrollList->clearRows();
|
|
|
|
std::vector<FSPoserAnimator::FSPoserJoint>::const_iterator poserJoint_iter;
|
|
for (poserJoint_iter = mPoserAnimator.PoserJoints.begin();
|
|
poserJoint_iter != mPoserAnimator.PoserJoints.end();
|
|
++poserJoint_iter)
|
|
{
|
|
LLSD row = createRowForJoint(poserJoint_iter->jointName(), false);
|
|
if (!row)
|
|
continue;
|
|
|
|
LLScrollListItem *item = nullptr;
|
|
bool hasListHeader = hasString(XML_LIST_HEADER_STRING_PREFIX + poserJoint_iter->jointName());
|
|
|
|
switch (poserJoint_iter->boneType())
|
|
{
|
|
case WHOLEAVATAR:
|
|
item = mEntireAvJointScroll->addElement(row);
|
|
mEntireAvJointScroll->selectFirstItem();
|
|
break;
|
|
|
|
case BODY:
|
|
if (hasListHeader)
|
|
addHeaderRowToScrollList(poserJoint_iter->jointName(), mBodyJointsScrollList);
|
|
|
|
item = mBodyJointsScrollList->addElement(row);
|
|
break;
|
|
|
|
case FACE:
|
|
if (hasListHeader)
|
|
addHeaderRowToScrollList(poserJoint_iter->jointName(), mFaceJointsScrollList);
|
|
|
|
item = mFaceJointsScrollList->addElement(row);
|
|
break;
|
|
|
|
case HANDS:
|
|
if (hasListHeader)
|
|
addHeaderRowToScrollList(poserJoint_iter->jointName(), mHandJointsScrollList);
|
|
|
|
item = mHandJointsScrollList->addElement(row);
|
|
break;
|
|
|
|
case MISC:
|
|
if (hasListHeader)
|
|
addHeaderRowToScrollList(poserJoint_iter->jointName(), mMiscJointsScrollList);
|
|
|
|
item = mMiscJointsScrollList->addElement(row);
|
|
break;
|
|
|
|
case COL_VOLUMES:
|
|
if (hasListHeader)
|
|
addHeaderRowToScrollList(poserJoint_iter->jointName(), mCollisionVolumesScrollList);
|
|
|
|
item = mCollisionVolumesScrollList->addElement(row);
|
|
break;
|
|
}
|
|
|
|
if (item)
|
|
item->setUserdata((void*) &*poserJoint_iter);
|
|
}
|
|
|
|
refreshTextHighlightingOnJointScrollLists();
|
|
}
|
|
|
|
void FSFloaterPoser::addHeaderRowToScrollList(const std::string& jointName, LLScrollListCtrl* bodyJointsScrollList)
|
|
{
|
|
LLSD headerRow = createRowForJoint(jointName, true);
|
|
if (!headerRow)
|
|
return;
|
|
|
|
LLScrollListItem *hdrRow = bodyJointsScrollList->addElement(headerRow);
|
|
hdrRow->setEnabled(FALSE);
|
|
}
|
|
|
|
LLSD FSFloaterPoser::createRowForJoint(const std::string& jointName, bool isHeaderRow)
|
|
{
|
|
if (jointName.empty())
|
|
return NULL;
|
|
|
|
std::string headerValue = "";
|
|
if (isHeaderRow && hasString("icon_category"))
|
|
headerValue = getString("icon_category");
|
|
|
|
std::string jointValue = jointName;
|
|
std::string parameterName = (isHeaderRow ? XML_LIST_HEADER_STRING_PREFIX : XML_LIST_TITLE_STRING_PREFIX) + jointName;
|
|
if (hasString(parameterName))
|
|
jointValue = getString(parameterName);
|
|
|
|
LLSD row;
|
|
row["columns"][COL_ICON]["column"] = "icon";
|
|
row["columns"][COL_ICON]["type"] = "icon";
|
|
row["columns"][COL_ICON]["value"] = headerValue;
|
|
row["columns"][COL_NAME]["column"] = "joint";
|
|
row["columns"][COL_NAME]["value"] = jointValue;
|
|
|
|
return row;
|
|
}
|
|
|
|
void FSFloaterPoser::onToggleLoadSavePanel()
|
|
{
|
|
if (isMinimized())
|
|
return;
|
|
|
|
// Get the load/save button toggle state, find the load/save panel, and set its visibility
|
|
bool loadSavePanelExpanded = mToggleLoadSavePanelBtn->getValue().asBoolean();
|
|
mPosesLoadSavePnl->setVisible(loadSavePanelExpanded);
|
|
mLoadPosesBtn->setVisible(loadSavePanelExpanded);
|
|
mSavePosesBtn->setVisible(loadSavePanelExpanded);
|
|
mBrowserFolderBtn->setVisible(loadSavePanelExpanded);
|
|
|
|
// change the width of the Poser panel for the (dis)appearance of the load/save panel
|
|
S32 currentWidth = getRect().getWidth();
|
|
S32 loadSavePanelWidth = mPosesLoadSavePnl->getRect().getWidth();
|
|
|
|
S32 poserFloaterHeight = getRect().getHeight();
|
|
S32 poserFloaterWidth = loadSavePanelExpanded ? currentWidth + loadSavePanelWidth : currentWidth - loadSavePanelWidth;
|
|
|
|
if (poserFloaterWidth < 0)
|
|
return;
|
|
|
|
reshape(poserFloaterWidth, poserFloaterHeight);
|
|
|
|
if (loadSavePanelExpanded)
|
|
refreshPoseScroll(mPosesScrollList);
|
|
}
|
|
|
|
void FSFloaterPoser::onToggleMirrorChange()
|
|
{
|
|
setRotationChangeButtons(true, false);
|
|
}
|
|
|
|
void FSFloaterPoser::onToggleSympatheticChange()
|
|
{
|
|
setRotationChangeButtons(false, true);
|
|
}
|
|
|
|
void FSFloaterPoser::setRotationChangeButtons(bool togglingMirror, bool togglingSympathetic)
|
|
{
|
|
if (togglingSympathetic)
|
|
mToggleMirrorRotationBtn->setValue(false);
|
|
|
|
if (togglingMirror)
|
|
mToggleSympatheticRotationBtn->setValue(false);
|
|
|
|
refreshTrackpadCursor();
|
|
}
|
|
|
|
void FSFloaterPoser::onUndoLastRotation()
|
|
{
|
|
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.undoLastJointRotation(avatar, *item, getUiSelectedBoneDeflectionStyle());
|
|
}
|
|
|
|
enableOrDisableRedoButton();
|
|
refreshRotationSliders();
|
|
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());
|
|
}
|
|
|
|
refreshAdvancedPositionSliders();
|
|
refreshAvatarPositionSliders();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
refreshAdvancedScaleSliders();
|
|
}
|
|
|
|
void FSFloaterPoser::onSetAvatarToTpose()
|
|
{
|
|
if (notDoubleClicked())
|
|
return;
|
|
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
setSavePosesButtonText(false);
|
|
mPoserAnimator.setAllAvatarStartingRotationsToZero(avatar);
|
|
refreshTextHighlightingOnJointScrollLists();
|
|
}
|
|
|
|
void FSFloaterPoser::onResetPosition()
|
|
{
|
|
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.resetJointPosition(avatar, *item, getUiSelectedBoneDeflectionStyle());
|
|
}
|
|
|
|
refreshAdvancedPositionSliders();
|
|
refreshAvatarPositionSliders();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
refreshAdvancedScaleSliders();
|
|
}
|
|
|
|
void FSFloaterPoser::onRedoLastRotation()
|
|
{
|
|
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.redoLastJointRotation(avatar, *item, getUiSelectedBoneDeflectionStyle());
|
|
}
|
|
|
|
enableOrDisableRedoButton();
|
|
refreshRotationSliders();
|
|
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());
|
|
}
|
|
|
|
refreshAdvancedPositionSliders();
|
|
refreshAvatarPositionSliders();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
refreshAdvancedScaleSliders();
|
|
}
|
|
|
|
void FSFloaterPoser::enableOrDisableRedoButton()
|
|
{
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
auto selectedJoints = getUiSelectedPoserJoints();
|
|
if (selectedJoints.size() < 1)
|
|
return;
|
|
|
|
bool shouldEnableRedoButton = false;
|
|
for (auto item : selectedJoints)
|
|
{
|
|
bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item);
|
|
if (currentlyPosing)
|
|
shouldEnableRedoButton |= mPoserAnimator.canRedoJointRotation(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<FSPoserAnimator::FSPoserJoint*> FSFloaterPoser::getUiSelectedPoserJoints() const
|
|
{
|
|
std::vector<FSPoserAnimator::FSPoserJoint*> joints;
|
|
|
|
auto activeTab = mJointsTabs->getCurrentPanel();
|
|
if (!activeTab)
|
|
{
|
|
return joints;
|
|
}
|
|
|
|
LLScrollListCtrl* scrollList{ nullptr };
|
|
|
|
if (activeTab == mPositionRotationPnl)
|
|
{
|
|
mEntireAvJointScroll->selectFirstItem();
|
|
scrollList = mEntireAvJointScroll;
|
|
}
|
|
else if (activeTab == mBodyJointsPnl)
|
|
{
|
|
scrollList = mBodyJointsScrollList;
|
|
}
|
|
else if (activeTab == mFaceJointsPnl)
|
|
{
|
|
scrollList = mFaceJointsScrollList;
|
|
}
|
|
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;
|
|
}
|
|
|
|
for (auto item : scrollList->getAllSelected())
|
|
{
|
|
auto* userData = static_cast<FSPoserAnimator::FSPoserJoint*>(item->getUserdata());
|
|
if (userData)
|
|
{
|
|
joints.push_back(userData);
|
|
}
|
|
}
|
|
|
|
return joints;
|
|
}
|
|
|
|
E_RotationStyle FSFloaterPoser::getUiSelectedBoneRotationStyle(const std::string& jointName) const
|
|
{
|
|
if (jointName.empty())
|
|
return ABSOLUTE_ROT;
|
|
|
|
bool hasRotationStylePreferenceParameter = hasString(XML_JOINT_DELTAROT_STRING_PREFIX + jointName);
|
|
if (!hasRotationStylePreferenceParameter)
|
|
return ABSOLUTE_ROT;
|
|
|
|
std::string paramValue = getString(XML_JOINT_DELTAROT_STRING_PREFIX + jointName);
|
|
if (paramValue == "true")
|
|
return DELTAIC_ROT;
|
|
|
|
return ABSOLUTE_ROT;
|
|
}
|
|
|
|
E_BoneDeflectionStyles FSFloaterPoser::getUiSelectedBoneDeflectionStyle() const
|
|
{
|
|
bool isDelta = mToggleDeltaModeBtn->getValue().asBoolean();
|
|
|
|
if (mToggleMirrorRotationBtn->getValue().asBoolean())
|
|
return isDelta ? MIRROR_DELTA : MIRROR;
|
|
|
|
if (mToggleSympatheticRotationBtn->getValue().asBoolean())
|
|
return isDelta ? SYMPATHETIC_DELTA : SYMPATHETIC;
|
|
|
|
return isDelta ? DELTAMODE : NONE;
|
|
}
|
|
|
|
LLVOAvatar* FSFloaterPoser::getUiSelectedAvatar() const
|
|
{
|
|
LLScrollListItem* item = mAvatarSelectionScrollList->getFirstSelected();
|
|
if (!item)
|
|
return nullptr;
|
|
|
|
LLScrollListCell* cell = item->getColumn(COL_UUID);
|
|
if (!cell)
|
|
return nullptr;
|
|
|
|
LLUUID selectedAvatarId = cell->getValue().asUUID();
|
|
|
|
return getAvatarByUuid(selectedAvatarId);
|
|
}
|
|
|
|
void FSFloaterPoser::setUiSelectedAvatar(const LLUUID& avatarToSelect)
|
|
{
|
|
for (auto listItem : mAvatarSelectionScrollList->getAllData())
|
|
{
|
|
LLScrollListCell* cell = listItem->getColumn(COL_UUID);
|
|
if (!cell)
|
|
continue;
|
|
|
|
LLUUID avatarId = cell->getValue().asUUID();
|
|
if (avatarId != avatarToSelect)
|
|
continue;
|
|
|
|
listItem->setSelected(true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FSFloaterPoser::setPoseSaveFileTextBoxToUiSelectedAvatarSaveFileName()
|
|
{
|
|
LLScrollListItem* item = mAvatarSelectionScrollList->getFirstSelected();
|
|
if (!item)
|
|
return;
|
|
|
|
LLScrollListCell* cell = item->getColumn(COL_SAVE);
|
|
if (!cell)
|
|
return;
|
|
|
|
std::string lastSetName = cell->getValue().asString();
|
|
if (lastSetName.empty())
|
|
return;
|
|
|
|
LLStringExplicit name = LLStringExplicit(lastSetName);
|
|
mPoseSaveNameEditor->setText(name);
|
|
}
|
|
|
|
void FSFloaterPoser::setUiSelectedAvatarSaveFileName(const std::string& saveFileName)
|
|
{
|
|
LLScrollListItem* item = mAvatarSelectionScrollList->getFirstSelected();
|
|
if (!item)
|
|
return;
|
|
|
|
LLScrollListCell* cell = item->getColumn(COL_SAVE);
|
|
if (!cell)
|
|
return;
|
|
|
|
return cell->setValue(saveFileName);
|
|
}
|
|
|
|
LLVOAvatar* FSFloaterPoser::getAvatarByUuid(const LLUUID& avatarToFind) const
|
|
{
|
|
for (LLCharacter* character : LLCharacter::sInstances)
|
|
{
|
|
if (avatarToFind != character->getID())
|
|
continue;
|
|
|
|
LLVOAvatar* avatar = dynamic_cast<LLVOAvatar*>(character);
|
|
return avatar;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void FSFloaterPoser::onAdvancedPositionSet()
|
|
{
|
|
F32 posX = mAdvPosXSlider->getValueF32();
|
|
F32 posY = mAdvPosYSlider->getValueF32();
|
|
F32 posZ = mAdvPosZSlider->getValueF32();
|
|
|
|
mAdvPosXSpnr->setValue(posX);
|
|
mInOutSpnr->setValue(posX);
|
|
mAdvPosYSpnr->setValue(posY);
|
|
mLeftRightSpnr->setValue(posY);
|
|
mAdvPosZSpnr->setValue(posZ);
|
|
mUpDownSpnr->setValue(posZ);
|
|
|
|
setSelectedJointsPosition(posX, posY, posZ);
|
|
refreshAvatarPositionSliders();
|
|
}
|
|
|
|
void FSFloaterPoser::onAdvancedScaleSet()
|
|
{
|
|
F32 scX = mAdvScaleXSlider->getValueF32();
|
|
F32 scY = mAdvScaleYSlider->getValueF32();
|
|
F32 scZ = mAdvScaleZSlider->getValueF32();
|
|
|
|
mScaleXSpnr->setValue(scX);
|
|
mScaleYSpnr->setValue(scY);
|
|
mScaleZSpnr->setValue(scZ);
|
|
|
|
setSelectedJointsScale(scX, scY, scZ);
|
|
}
|
|
|
|
void FSFloaterPoser::onAvatarPositionSet()
|
|
{
|
|
F32 posX = mPosXSlider->getValueF32();
|
|
F32 posY = mPosYSlider->getValueF32();
|
|
F32 posZ = mPosZSlider->getValueF32();
|
|
|
|
mAdvPosXSpnr->setValue(posX);
|
|
mInOutSpnr->setValue(posX);
|
|
mAdvPosYSpnr->setValue(posY);
|
|
mLeftRightSpnr->setValue(posY);
|
|
mAdvPosZSpnr->setValue(posZ);
|
|
mUpDownSpnr->setValue(posZ);
|
|
|
|
setSelectedJointsPosition(posX, posY, posZ);
|
|
refreshAdvancedPositionSliders();
|
|
}
|
|
|
|
void FSFloaterPoser::onLimbTrackballChanged()
|
|
{
|
|
LLVector3 trackPadPos, trackPadDeltaPos;
|
|
LLSD position = mAvatarTrackball->getValue();
|
|
LLSD deltaPosition = mAvatarTrackball->getValueDelta();
|
|
|
|
if (position.isArray() && position.size() == 3 && deltaPosition.isArray() && deltaPosition.size() == 3)
|
|
{
|
|
trackPadPos.setValue(position);
|
|
trackPadDeltaPos.setValue(deltaPosition);
|
|
}
|
|
else
|
|
return;
|
|
|
|
F32 trackPadSensitivity = llmax(gSavedSettings.getF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY), 0.0001f);
|
|
|
|
trackPadPos.mV[VX] *= trackPadSensitivity;
|
|
trackPadPos.mV[VY] *= trackPadSensitivity;
|
|
|
|
trackPadPos.mV[VX] = unWrapScale(trackPadPos.mV[VX]) * NormalTrackpadRangeInRads;
|
|
trackPadPos.mV[VY] = unWrapScale(trackPadPos.mV[VY]) * NormalTrackpadRangeInRads;
|
|
trackPadPos.mV[VZ] = unWrapScale(trackPadPos.mV[VZ]) * NormalTrackpadRangeInRads;
|
|
|
|
trackPadDeltaPos[VX] *= NormalTrackpadRangeInRads * trackPadSensitivity;
|
|
trackPadDeltaPos[VY] *= NormalTrackpadRangeInRads * trackPadSensitivity;
|
|
trackPadDeltaPos[VZ] *= NormalTrackpadRangeInRads;
|
|
|
|
setSelectedJointsRotation(trackPadPos, trackPadDeltaPos);
|
|
|
|
// WARNING!
|
|
// as tempting as it is to refactor the following to refreshRotationSliders(), don't.
|
|
// getRotationOfFirstSelectedJoint/setSelectedJointsRotation are
|
|
// not necessarily symmetric functions (see their remarks).
|
|
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());
|
|
}
|
|
|
|
F32 FSFloaterPoser::unWrapScale(F32 scale)
|
|
{
|
|
if (scale > -1.f && scale < 1.f)
|
|
return scale;
|
|
|
|
F32 result = fmodf(scale, 100.f); // to avoid time consuming while loops
|
|
while (result > 1)
|
|
result -= 2;
|
|
while (result < -1)
|
|
result += 2;
|
|
|
|
return result;
|
|
}
|
|
|
|
void FSFloaterPoser::onYawPitchRollSliderChanged()
|
|
{
|
|
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;
|
|
|
|
deltaRotation = absoluteRotation - mLastSliderRotation;
|
|
mLastSliderRotation = absoluteRotation;
|
|
|
|
setSelectedJointsRotation(absoluteRotation, deltaRotation);
|
|
|
|
// WARNING!
|
|
// as tempting as it is to refactor the following to refreshTrackpadCursor(), don't.
|
|
// getRotationOfFirstSelectedJoint/setSelectedJointsRotation are
|
|
// not necessarily symmetric functions (see their remarks).
|
|
F32 trackPadSensitivity = llmax(gSavedSettings.getF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY), 0.0001f);
|
|
absoluteRotation.mV[VX] /= trackPadSensitivity;
|
|
absoluteRotation.mV[VY] /= trackPadSensitivity;
|
|
|
|
absoluteRotation.mV[VX] /= NormalTrackpadRangeInRads;
|
|
absoluteRotation.mV[VY] /= NormalTrackpadRangeInRads;
|
|
absoluteRotation.mV[VZ] /= NormalTrackpadRangeInRads;
|
|
|
|
mAvatarTrackball->setValue(absoluteRotation.getValue());
|
|
|
|
mYawSpnr->setValue(mLimbYawSlider->getValueF32());
|
|
mPitchSpnr->setValue(mLimbPitchSlider->getValueF32());
|
|
mRollSpnr->setValue(mLimbRollSlider->getValueF32());
|
|
}
|
|
|
|
void FSFloaterPoser::onAdjustTrackpadSensitivity()
|
|
{
|
|
gSavedSettings.setF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY, mTrackpadSensitivitySlider->getValueF32());
|
|
refreshTrackpadCursor();
|
|
}
|
|
|
|
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 trackPadSensitivity = llmax(gSavedSettings.getF32(POSER_TRACKPAD_SENSITIVITY_SAVE_KEY), 0.0001f);
|
|
axis1 /= trackPadSensitivity;
|
|
axis2 /= trackPadSensitivity;
|
|
|
|
mAvatarTrackball->setValue(axis1, axis2, axis3);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This only sets the position sliders of the 'basic' view (not the advanced sliders).
|
|
/// </summary>
|
|
void FSFloaterPoser::refreshAvatarPositionSliders()
|
|
{
|
|
auto activeTab = mJointsTabs->getCurrentPanel();
|
|
if (!activeTab)
|
|
return;
|
|
|
|
if (activeTab != mPositionRotationPnl)
|
|
return; // if the active tab isn't the av position one, don't set anything.
|
|
|
|
LLVector3 position = getPositionOfFirstSelectedJoint();
|
|
mPosXSlider->setValue(position.mV[VX]);
|
|
mPosYSlider->setValue(position.mV[VY]);
|
|
mPosZSlider->setValue(position.mV[VZ]);
|
|
}
|
|
|
|
void FSFloaterPoser::refreshRotationSliders()
|
|
{
|
|
LLVector3 rotation = getRotationOfFirstSelectedJoint();
|
|
|
|
mLastSliderRotation = rotation;
|
|
mLimbYawSlider->setValue(rotation.mV[VX] *= RAD_TO_DEG);
|
|
mLimbPitchSlider->setValue(rotation.mV[VY] *= RAD_TO_DEG);
|
|
mLimbRollSlider->setValue(rotation.mV[VZ] *= RAD_TO_DEG);
|
|
}
|
|
|
|
void FSFloaterPoser::refreshAdvancedPositionSliders()
|
|
{
|
|
LLVector3 position = getPositionOfFirstSelectedJoint();
|
|
|
|
mAdvPosXSlider->setValue(position.mV[VX]);
|
|
mAdvPosYSlider->setValue(position.mV[VY]);
|
|
mAdvPosZSlider->setValue(position.mV[VZ]);
|
|
}
|
|
|
|
void FSFloaterPoser::refreshAdvancedScaleSliders()
|
|
{
|
|
LLVector3 rotation = getScaleOfFirstSelectedJoint();
|
|
|
|
mAdvScaleXSlider->setValue(rotation.mV[VX]);
|
|
mAdvScaleYSlider->setValue(rotation.mV[VY]);
|
|
mAdvScaleZSlider->setValue(rotation.mV[VZ]);
|
|
}
|
|
|
|
void FSFloaterPoser::setSelectedJointsPosition(F32 x, F32 y, F32 z)
|
|
{
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
LLVector3 vec3 = LLVector3(x, y, z);
|
|
E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle();
|
|
|
|
for (auto item : getUiSelectedPoserJoints())
|
|
{
|
|
bool currentlyPosingJoint = mPoserAnimator.isPosingAvatarJoint(avatar, *item);
|
|
if (!currentlyPosingJoint)
|
|
continue;
|
|
|
|
mPoserAnimator.setJointPosition(avatar, item, vec3, defl);
|
|
}
|
|
}
|
|
|
|
void FSFloaterPoser::setSelectedJointsRotation(const LLVector3& absoluteRot, const LLVector3& deltaRot)
|
|
{
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
auto selectedJoints = getUiSelectedPoserJoints();
|
|
bool savingToExternal = getWhetherToResetBaseRotationOnEdit();
|
|
E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle();
|
|
|
|
for (auto item : selectedJoints)
|
|
{
|
|
bool currentlyPosingJoint = mPoserAnimator.isPosingAvatarJoint(avatar, *item);
|
|
if (!currentlyPosingJoint)
|
|
continue;
|
|
|
|
auto oppositeJoint = mPoserAnimator.getPoserJointByName(item->mirrorJointName());
|
|
if (oppositeJoint)
|
|
{
|
|
bool oppositeJointAlsoSelectedOnUi =
|
|
std::find(selectedJoints.begin(), selectedJoints.end(), oppositeJoint) != selectedJoints.end();
|
|
|
|
bool deflectionDoesOppositeLimbs = !(defl == NONE || defl == DELTAMODE);
|
|
if (oppositeJointAlsoSelectedOnUi && deflectionDoesOppositeLimbs && item->dontFlipOnMirror())
|
|
continue;
|
|
}
|
|
|
|
mPoserAnimator.setJointRotation(avatar, item, absoluteRot, deltaRot, defl,
|
|
getJointTranslation(item->jointName()), getJointNegation(item->jointName()), savingToExternal,
|
|
getUiSelectedBoneRotationStyle(item->jointName()));
|
|
}
|
|
|
|
if (savingToExternal)
|
|
refreshTextHighlightingOnJointScrollLists();
|
|
}
|
|
|
|
void FSFloaterPoser::setSelectedJointsScale(F32 x, F32 y, F32 z)
|
|
{
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return;
|
|
|
|
LLVector3 vec3 = LLVector3(x, y, z);
|
|
E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle();
|
|
|
|
for (auto item : getUiSelectedPoserJoints())
|
|
{
|
|
bool currentlyPosingJoint = mPoserAnimator.isPosingAvatarJoint(avatar, *item);
|
|
if (!currentlyPosingJoint)
|
|
continue;
|
|
|
|
mPoserAnimator.setJointScale(avatar, item, vec3, defl);
|
|
}
|
|
}
|
|
|
|
LLVector3 FSFloaterPoser::getRotationOfFirstSelectedJoint() const
|
|
{
|
|
LLVector3 rotation;
|
|
auto selectedJoints = getUiSelectedPoserJoints();
|
|
if (selectedJoints.size() < 1)
|
|
return rotation;
|
|
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return rotation;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return rotation;
|
|
|
|
rotation = mPoserAnimator.getJointRotation(avatar, *selectedJoints.front(), getJointTranslation(selectedJoints.front()->jointName()),
|
|
getJointNegation(selectedJoints.front()->jointName()));
|
|
|
|
return rotation;
|
|
}
|
|
|
|
LLVector3 FSFloaterPoser::getPositionOfFirstSelectedJoint() const
|
|
{
|
|
LLVector3 position;
|
|
auto selectedJoints = getUiSelectedPoserJoints();
|
|
if (selectedJoints.size() < 1)
|
|
return position;
|
|
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return position;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return position;
|
|
|
|
position = mPoserAnimator.getJointPosition(avatar, *selectedJoints.front());
|
|
return position;
|
|
}
|
|
|
|
LLVector3 FSFloaterPoser::getScaleOfFirstSelectedJoint() const
|
|
{
|
|
LLVector3 scale;
|
|
auto selectedJoints = getUiSelectedPoserJoints();
|
|
if (selectedJoints.size() < 1)
|
|
return scale;
|
|
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
if (!avatar)
|
|
return scale;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return scale;
|
|
|
|
scale = mPoserAnimator.getJointScale(avatar, *selectedJoints.front());
|
|
return scale;
|
|
}
|
|
|
|
void FSFloaterPoser::onJointTabSelect()
|
|
{
|
|
refreshAvatarPositionSliders();
|
|
refreshRotationSliders();
|
|
refreshTrackpadCursor();
|
|
enableOrDisableRedoButton();
|
|
onClickSetBaseRotZero();
|
|
|
|
if (mToggleAdvancedPanelBtn->getValue().asBoolean())
|
|
{
|
|
refreshAdvancedPositionSliders();
|
|
refreshAdvancedScaleSliders();
|
|
}
|
|
}
|
|
|
|
E_BoneAxisTranslation FSFloaterPoser::getJointTranslation(const std::string& jointName) const
|
|
{
|
|
if (jointName.empty())
|
|
return SWAP_NOTHING;
|
|
|
|
bool hasTransformParameter = hasString(XML_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
if (!hasTransformParameter)
|
|
return SWAP_NOTHING;
|
|
|
|
std::string paramValue = getString(XML_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
|
|
if (strstr(paramValue.c_str(), "SWAP_YAW_AND_ROLL"))
|
|
return SWAP_YAW_AND_ROLL;
|
|
else if (strstr(paramValue.c_str(), "SWAP_YAW_AND_PITCH"))
|
|
return SWAP_YAW_AND_PITCH;
|
|
else if (strstr(paramValue.c_str(), "SWAP_ROLL_AND_PITCH"))
|
|
return SWAP_ROLL_AND_PITCH;
|
|
else if (strstr(paramValue.c_str(), "SWAP_X2Y_Y2Z_Z2X"))
|
|
return SWAP_X2Y_Y2Z_Z2X;
|
|
else if (strstr(paramValue.c_str(), "SWAP_X2Z_Y2X_Z2Y"))
|
|
return SWAP_X2Z_Y2X_Z2Y;
|
|
else
|
|
return SWAP_NOTHING;
|
|
}
|
|
|
|
S32 FSFloaterPoser::getJointNegation(const std::string& jointName) const
|
|
{
|
|
S32 result = NEGATE_NOTHING;
|
|
|
|
if (jointName.empty())
|
|
return result;
|
|
|
|
bool hasTransformParameter = hasString(XML_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
if (!hasTransformParameter)
|
|
return result;
|
|
|
|
std::string paramValue = getString(XML_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
|
|
if (strstr(paramValue.c_str(), "NEGATE_YAW"))
|
|
result |= NEGATE_YAW;
|
|
if (strstr(paramValue.c_str(), "NEGATE_PITCH"))
|
|
result |= NEGATE_PITCH;
|
|
if (strstr(paramValue.c_str(), "NEGATE_ROLL"))
|
|
result |= NEGATE_ROLL;
|
|
if (strstr(paramValue.c_str(), "NEGATE_ALL"))
|
|
return NEGATE_ALL;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// An event handler for selecting an avatar or animesh on the POSES_AVATAR_SCROLL_LIST_NAME.
|
|
/// In general this will refresh the views for joints or their proxies, and (dis/en)able elements of the view.
|
|
/// </summary>
|
|
void FSFloaterPoser::onAvatarSelect()
|
|
{
|
|
LLVOAvatar* avatar = getUiSelectedAvatar();
|
|
mStartStopPosingBtn->setEnabled(couldAnimateAvatar(avatar));
|
|
|
|
bool arePosingSelected = mPoserAnimator.isPosingAvatar(avatar);
|
|
mStartStopPosingBtn->setValue(arePosingSelected);
|
|
mSetToTposeButton->setEnabled(arePosingSelected);
|
|
poseControlsEnable(arePosingSelected);
|
|
refreshTextHighlightingOnAvatarScrollList();
|
|
refreshTextHighlightingOnJointScrollLists();
|
|
onJointTabSelect();
|
|
setPoseSaveFileTextBoxToUiSelectedAvatarSaveFileName();
|
|
}
|
|
|
|
uuid_vec_t FSFloaterPoser::getNearbyAvatarsAndAnimeshes() const
|
|
{
|
|
uuid_vec_t avatar_ids;
|
|
|
|
for (LLCharacter* character : LLCharacter::sInstances)
|
|
{
|
|
LLVOAvatar* avatar = dynamic_cast<LLVOAvatar*>(character);
|
|
if (!havePermissionToAnimateAvatar(avatar))
|
|
continue;
|
|
|
|
avatar_ids.emplace_back(character->getID());
|
|
}
|
|
|
|
return avatar_ids;
|
|
}
|
|
|
|
uuid_vec_t FSFloaterPoser::getCurrentlyListedAvatarsAndAnimeshes() const
|
|
{
|
|
uuid_vec_t avatar_ids;
|
|
|
|
for (auto listItem : mAvatarSelectionScrollList->getAllData())
|
|
{
|
|
LLScrollListCell* cell = listItem->getColumn(COL_UUID);
|
|
if (!cell)
|
|
continue;
|
|
|
|
LLUUID avatarId = cell->getValue().asUUID();
|
|
avatar_ids.emplace_back(avatarId);
|
|
}
|
|
|
|
return avatar_ids;
|
|
}
|
|
|
|
S32 FSFloaterPoser::getAvatarListIndexForUuid(const LLUUID& toFind) const
|
|
{
|
|
S32 result = -1;
|
|
for (auto listItem : mAvatarSelectionScrollList->getAllData())
|
|
{
|
|
result++;
|
|
|
|
LLScrollListCell* cell = listItem->getColumn(COL_UUID);
|
|
if (!cell)
|
|
continue;
|
|
|
|
LLUUID avatarId = cell->getValue().asUUID();
|
|
if (avatarId == toFind)
|
|
return result;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
void FSFloaterPoser::onAvatarsRefresh()
|
|
{
|
|
uuid_vec_t avatarsToAddToList, avatarsToRemoveFromList;
|
|
uuid_vec_t nearbyAvatarIds = getNearbyAvatarsAndAnimeshes();
|
|
uuid_vec_t currentlyListedAvatars = getCurrentlyListedAvatarsAndAnimeshes();
|
|
LLCommonUtils::computeDifference(nearbyAvatarIds, currentlyListedAvatars, avatarsToAddToList, avatarsToRemoveFromList);
|
|
|
|
for (LLUUID toRemove : avatarsToRemoveFromList)
|
|
{
|
|
S32 indexToRemove = getAvatarListIndexForUuid(toRemove);
|
|
if (indexToRemove >= 0)
|
|
mAvatarSelectionScrollList->deleteSingleItem(indexToRemove);
|
|
}
|
|
|
|
std::string iconCatagoryName = "Inv_BodyShape";
|
|
if (hasString("icon_category"))
|
|
iconCatagoryName = getString("icon_category");
|
|
|
|
std::string iconObjectName = "Inv_Object";
|
|
if (hasString("icon_object"))
|
|
iconObjectName = getString("icon_object");
|
|
|
|
// Add non-Animesh avatars
|
|
for (LLCharacter *character : LLCharacter::sInstances)
|
|
{
|
|
LLUUID uuid = character->getID();
|
|
if (std::find(avatarsToAddToList.begin(), avatarsToAddToList.end(), uuid) == avatarsToAddToList.end())
|
|
continue;
|
|
|
|
LLVOAvatar *avatar = dynamic_cast<LLVOAvatar *>(character);
|
|
if (!couldAnimateAvatar(avatar))
|
|
continue;
|
|
|
|
if (avatar->isControlAvatar())
|
|
continue;
|
|
|
|
LLAvatarName av_name;
|
|
if (!LLAvatarNameCache::get(uuid, &av_name))
|
|
continue;
|
|
|
|
LLSD row;
|
|
row["columns"][COL_ICON]["column"] = "icon";
|
|
row["columns"][COL_ICON]["type"] = "icon";
|
|
row["columns"][COL_ICON]["value"] = iconCatagoryName;
|
|
row["columns"][COL_NAME]["column"] = "name";
|
|
row["columns"][COL_NAME]["value"] = av_name.getDisplayName();
|
|
row["columns"][COL_UUID]["column"] = "uuid";
|
|
row["columns"][COL_UUID]["value"] = uuid;
|
|
row["columns"][COL_SAVE]["column"] = "saveFileName";
|
|
row["columns"][COL_SAVE]["value"] = "";
|
|
LLScrollListItem* item = mAvatarSelectionScrollList->addElement(row);
|
|
}
|
|
|
|
// Add Animesh avatars
|
|
for (auto character : LLControlAvatar::sInstances)
|
|
{
|
|
LLUUID uuid = character->getID();
|
|
if (std::find(avatarsToAddToList.begin(), avatarsToAddToList.end(), uuid) == avatarsToAddToList.end())
|
|
continue;
|
|
|
|
LLControlAvatar *avatar = dynamic_cast<LLControlAvatar *>(character);
|
|
if (!couldAnimateAvatar(avatar))
|
|
continue;
|
|
|
|
LLAvatarName av_name;
|
|
std::string animeshName = getControlAvatarName(avatar);
|
|
if (animeshName.empty())
|
|
continue;
|
|
|
|
LLSD row;
|
|
row["columns"][COL_ICON]["column"] = "icon";
|
|
row["columns"][COL_ICON]["type"] = "icon";
|
|
row["columns"][COL_ICON]["value"] = iconObjectName;
|
|
row["columns"][COL_NAME]["column"] = "name";
|
|
row["columns"][COL_NAME]["value"] = animeshName;
|
|
row["columns"][COL_UUID]["column"] = "uuid";
|
|
row["columns"][COL_UUID]["value"] = avatar->getID();
|
|
row["columns"][COL_SAVE]["column"] = "saveFileName";
|
|
row["columns"][COL_SAVE]["value"] = "";
|
|
mAvatarSelectionScrollList->addElement(row);
|
|
}
|
|
|
|
mAvatarSelectionScrollList->updateLayout();
|
|
refreshTextHighlightingOnAvatarScrollList();
|
|
}
|
|
|
|
std::string FSFloaterPoser::getControlAvatarName(const LLControlAvatar* avatar)
|
|
{
|
|
if (!avatar)
|
|
return "";
|
|
|
|
const LLVOVolume* rootVolume = avatar->mRootVolp;
|
|
const LLViewerObject* rootEditObject = (rootVolume) ? rootVolume->getRootEdit() : NULL;
|
|
if (!rootEditObject)
|
|
return "";
|
|
|
|
const LLViewerInventoryItem* attachedItem =
|
|
(rootEditObject->isAttachment()) ? gInventory.getItem(rootEditObject->getAttachmentItemID()) : NULL;
|
|
|
|
if (attachedItem)
|
|
return attachedItem->getName();
|
|
|
|
if (rootEditObject->permYouOwner())
|
|
return avatar->getFullname();
|
|
|
|
return "";
|
|
}
|
|
|
|
void FSFloaterPoser::refreshTextHighlightingOnAvatarScrollList()
|
|
{
|
|
for (auto listItem : mAvatarSelectionScrollList->getAllData())
|
|
{
|
|
LLScrollListCell* cell = listItem->getColumn(COL_UUID);
|
|
if (!cell)
|
|
continue;
|
|
|
|
LLUUID selectedAvatarId = cell->getValue().asUUID();
|
|
LLVOAvatar* listAvatar = getAvatarByUuid(selectedAvatarId);
|
|
|
|
if (mPoserAnimator.isPosingAvatar(listAvatar))
|
|
((LLScrollListText *) listItem->getColumn(COL_NAME))->setFontStyle(LLFontGL::BOLD);
|
|
else
|
|
((LLScrollListText *) listItem->getColumn(COL_NAME))->setFontStyle(LLFontGL::NORMAL);
|
|
}
|
|
}
|
|
|
|
void FSFloaterPoser::refreshTextHighlightingOnJointScrollLists()
|
|
{
|
|
LLVOAvatar *avatar = getUiSelectedAvatar();
|
|
|
|
addBoldToScrollList(mBodyJointsScrollList, avatar);
|
|
addBoldToScrollList(mFaceJointsScrollList, avatar);
|
|
addBoldToScrollList(mHandJointsScrollList, avatar);
|
|
addBoldToScrollList(mMiscJointsScrollList, avatar);
|
|
addBoldToScrollList(mCollisionVolumesScrollList, avatar);
|
|
}
|
|
|
|
void FSFloaterPoser::setSavePosesButtonText(bool setAsSaveDiff)
|
|
{
|
|
setAsSaveDiff ? mSavePosesBtn->setLabel("Save Diff") : mSavePosesBtn->setLabel("Save Pose");
|
|
}
|
|
|
|
void FSFloaterPoser::addBoldToScrollList(LLScrollListCtrl* list, LLVOAvatar* avatar)
|
|
{
|
|
if (!avatar)
|
|
return;
|
|
|
|
if (!list)
|
|
return;
|
|
|
|
std::string iconValue = "";
|
|
bool considerExternalFormatSaving = getWhetherToResetBaseRotationOnEdit();
|
|
|
|
if (considerExternalFormatSaving && hasString("icon_rotation_is_own_work"))
|
|
iconValue = getString("icon_rotation_is_own_work");
|
|
|
|
for (auto listItem : list->getAllData())
|
|
{
|
|
FSPoserAnimator::FSPoserJoint *userData = static_cast<FSPoserAnimator::FSPoserJoint *>(listItem->getUserdata());
|
|
if (!userData)
|
|
continue;
|
|
|
|
if (considerExternalFormatSaving)
|
|
{
|
|
if (mPoserAnimator.baseRotationIsZero(avatar, *userData))
|
|
((LLScrollListText*) listItem->getColumn(COL_ICON))->setValue(iconValue);
|
|
else
|
|
((LLScrollListText*) listItem->getColumn(COL_ICON))->setValue("");
|
|
}
|
|
|
|
if (mPoserAnimator.isPosingAvatarJoint(avatar, *userData))
|
|
((LLScrollListText *) listItem->getColumn(COL_NAME))->setFontStyle(LLFontGL::BOLD);
|
|
else
|
|
((LLScrollListText *) listItem->getColumn(COL_NAME))->setFontStyle(LLFontGL::NORMAL);
|
|
}
|
|
}
|
|
|
|
bool FSFloaterPoser::savePoseToBvh(LLVOAvatar* avatar, const std::string& poseFileName)
|
|
{
|
|
if (poseFileName.empty())
|
|
return false;
|
|
|
|
if (!mPoserAnimator.isPosingAvatar(avatar))
|
|
return false;
|
|
|
|
bool writeSuccess = false;
|
|
|
|
try
|
|
{
|
|
std::string pathname = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY);
|
|
if (!gDirUtilp->fileExists(pathname))
|
|
{
|
|
LL_WARNS("Poser") << "Couldn't find folder: " << pathname << " - creating one." << LL_ENDL;
|
|
LLFile::mkdir(pathname);
|
|
}
|
|
|
|
std::string fullSavePath =
|
|
gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, poseFileName + POSE_EXTERNAL_FORMAT_FILE_EXT);
|
|
|
|
llofstream file;
|
|
file.open(fullSavePath.c_str());
|
|
if (!file.is_open())
|
|
{
|
|
LL_WARNS("Poser") << "Unable to save pose!" << LL_ENDL;
|
|
return false;
|
|
}
|
|
|
|
writeSuccess = writePoseAsBvh(&file, avatar);
|
|
|
|
file.close();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
LL_WARNS("Posing") << "Exception caught in SaveToBVH: " << e.what() << LL_ENDL;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FSFloaterPoser::writePoseAsBvh(llofstream* fileStream, LLVOAvatar* avatar)
|
|
{
|
|
if (!fileStream || !avatar)
|
|
return false;
|
|
|
|
*fileStream << "HIERARCHY" << std::endl;
|
|
auto startingJoint = mPoserAnimator.getPoserJointByName("mPelvis");
|
|
writeBvhFragment(fileStream, avatar, startingJoint, 0);
|
|
*fileStream << "MOTION" << std::endl;
|
|
*fileStream << "Frames: 2" << std::endl;
|
|
*fileStream << "Frame Time: 1" << std::endl;
|
|
*fileStream << "0.000000 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0" << std::endl;
|
|
writeBvhMotion(fileStream, avatar, startingJoint);
|
|
*fileStream << std::endl;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FSFloaterPoser::writeBvhFragment(llofstream* fileStream, LLVOAvatar* avatar, const FSPoserAnimator::FSPoserJoint* joint, S32 tabStops)
|
|
{
|
|
if (!joint)
|
|
return false;
|
|
|
|
auto position = mPoserAnimator.getJointPosition(avatar, *joint);
|
|
auto saveAxis = getBvhJointTranslation(joint->jointName());
|
|
|
|
switch (joint->boneType())
|
|
{
|
|
case WHOLEAVATAR:
|
|
*fileStream << "ROOT " + joint->jointName() << std::endl;
|
|
*fileStream << "{" << std::endl;
|
|
*fileStream << getTabs(tabStops + 1) + "OFFSET " + joint->bvhOffset() << std::endl;
|
|
// <AS:chanayane> BVH fixes
|
|
*fileStream << getTabs(tabStops + 1) + "CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation" << std::endl;
|
|
// </AS:chanayane>
|
|
break;
|
|
|
|
default:
|
|
*fileStream << getTabs(tabStops) + "JOINT " + joint->jointName() << std::endl;
|
|
*fileStream << getTabs(tabStops) + "{" << std::endl;
|
|
*fileStream << getTabs(tabStops + 1) + "OFFSET " + joint->bvhOffset() << std::endl;
|
|
|
|
switch (saveAxis)
|
|
{
|
|
default:
|
|
case SWAP_NOTHING:
|
|
*fileStream << getTabs(tabStops + 1) + "CHANNELS 3 Xrotation Yrotation Zrotation" << std::endl;
|
|
break;
|
|
|
|
case SWAP_YAW_AND_ROLL:
|
|
*fileStream << getTabs(tabStops + 1) + "CHANNELS 3 Zrotation Yrotation Xrotation" << std::endl;
|
|
break;
|
|
|
|
case SWAP_ROLL_AND_PITCH:
|
|
*fileStream << getTabs(tabStops + 1) + "CHANNELS 3 Xrotation Zrotation Yrotation" << std::endl;
|
|
break;
|
|
|
|
case SWAP_X2Z_Y2X_Z2Y:
|
|
*fileStream << getTabs(tabStops + 1) + "CHANNELS 3 Yrotation Zrotation Xrotation" << std::endl;
|
|
break;
|
|
|
|
case SWAP_X2Y_Y2Z_Z2X:
|
|
*fileStream << getTabs(tabStops + 1) + "CHANNELS 3 Zrotation Xrotation Yrotation" << std::endl;
|
|
break;
|
|
|
|
case SWAP_YAW_AND_PITCH:
|
|
*fileStream << getTabs(tabStops + 1) + "CHANNELS 3 Yrotation Xrotation Zrotation" << std::endl;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
size_t numberOfBvhChildNodes = joint->bvhChildren().size();
|
|
if (numberOfBvhChildNodes > 0)
|
|
{
|
|
for (size_t index = 0; index != numberOfBvhChildNodes; ++index)
|
|
{
|
|
auto nextJoint = mPoserAnimator.getPoserJointByName(joint->bvhChildren()[index]);
|
|
writeBvhFragment(fileStream, avatar, nextJoint, tabStops + 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*fileStream << getTabs(tabStops + 1) + "End Site" << std::endl;
|
|
*fileStream << getTabs(tabStops + 1) + "{" << std::endl;
|
|
|
|
// append the 'end knot' magic number
|
|
if (strstr(joint->jointName().c_str(), "mHead"))
|
|
*fileStream << getTabs(tabStops + 2) + "OFFSET 0.000000 3.148289 0.000000" << std::endl;
|
|
if (strstr(joint->jointName().c_str(), "mWristLeft"))
|
|
*fileStream << getTabs(tabStops + 2) + "OFFSET 4.106464 0.000000 0.000000" << std::endl;
|
|
if (strstr(joint->jointName().c_str(), "mWristRight"))
|
|
*fileStream << getTabs(tabStops + 2) + "OFFSET -4.106464 0.000000 0.000000" << std::endl;
|
|
if (strstr(joint->jointName().c_str(), "mAnkleLeft"))
|
|
*fileStream << getTabs(tabStops + 2) + "OFFSET 0.000000 -2.463878 4.653993" << std::endl;
|
|
if (strstr(joint->jointName().c_str(), "mAnkleRight"))
|
|
*fileStream << getTabs(tabStops + 2) + "OFFSET 0.000000 -2.463878 4.653993" << std::endl;
|
|
|
|
*fileStream << getTabs(tabStops + 1) + "}" << std::endl;
|
|
}
|
|
|
|
*fileStream << getTabs(tabStops) + "}" << std::endl;
|
|
return true;
|
|
}
|
|
|
|
bool FSFloaterPoser::writeBvhMotion(llofstream* fileStream, LLVOAvatar* avatar, const FSPoserAnimator::FSPoserJoint* joint)
|
|
{
|
|
if (!joint)
|
|
return false;
|
|
|
|
auto rotation = mPoserAnimator.getJointRotation(avatar, *joint, SWAP_NOTHING, NEGATE_NOTHING);
|
|
auto position = mPoserAnimator.getJointPosition(avatar, *joint);
|
|
|
|
switch (joint->boneType())
|
|
{
|
|
case WHOLEAVATAR:
|
|
*fileStream << vec3ToXYZString(position) + " " + rotationToString(rotation);
|
|
break;
|
|
|
|
default:
|
|
*fileStream << " " + rotationToString(rotation);
|
|
break;
|
|
}
|
|
|
|
size_t numberOfBvhChildNodes = joint->bvhChildren().size();
|
|
for (size_t index = 0; index != numberOfBvhChildNodes; ++index)
|
|
{
|
|
auto nextJoint = mPoserAnimator.getPoserJointByName(joint->bvhChildren()[index]);
|
|
writeBvhMotion(fileStream, avatar, nextJoint);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string FSFloaterPoser::vec3ToXYZString(const LLVector3& val)
|
|
{
|
|
return std::to_string(val[VX]) + " " + std::to_string(val[VY]) + " " + std::to_string(val[VZ]);
|
|
}
|
|
|
|
std::string FSFloaterPoser::rotationToString(const LLVector3& val)
|
|
{
|
|
return std::to_string(val[VX] * RAD_TO_DEG) + " " + std::to_string(val[VY] * RAD_TO_DEG) + " " + std::to_string(val[VZ] * RAD_TO_DEG);
|
|
}
|
|
|
|
std::string FSFloaterPoser::getTabs(S32 numOfTabstops)
|
|
{
|
|
std::string tabSpaces;
|
|
for (S32 i = 0; i < numOfTabstops; i++)
|
|
tabSpaces += "\t";
|
|
|
|
return tabSpaces;
|
|
}
|
|
|
|
E_BoneAxisTranslation FSFloaterPoser::getBvhJointTranslation(const std::string& jointName) const
|
|
{
|
|
if (jointName.empty())
|
|
return SWAP_X2Y_Y2Z_Z2X;
|
|
|
|
bool hasTransformParameter = hasString(BVH_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
if (!hasTransformParameter)
|
|
return SWAP_X2Y_Y2Z_Z2X;
|
|
|
|
std::string paramValue = getString(BVH_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
|
|
if (strstr(paramValue.c_str(), "SWAP_YAW_AND_ROLL"))
|
|
return SWAP_YAW_AND_ROLL;
|
|
else if (strstr(paramValue.c_str(), "SWAP_YAW_AND_PITCH"))
|
|
return SWAP_YAW_AND_PITCH;
|
|
else if (strstr(paramValue.c_str(), "SWAP_ROLL_AND_PITCH"))
|
|
return SWAP_ROLL_AND_PITCH;
|
|
else if (strstr(paramValue.c_str(), "SWAP_X2Y_Y2Z_Z2X"))
|
|
return SWAP_X2Y_Y2Z_Z2X;
|
|
else if (strstr(paramValue.c_str(), "SWAP_X2Z_Y2X_Z2Y"))
|
|
return SWAP_X2Z_Y2X_Z2Y;
|
|
else
|
|
return SWAP_NOTHING;
|
|
}
|
|
|
|
S32 FSFloaterPoser::getBvhJointNegation(const std::string& jointName) const
|
|
{
|
|
S32 result = NEGATE_NOTHING;
|
|
|
|
if (jointName.empty())
|
|
return result;
|
|
|
|
bool hasTransformParameter = hasString(BVH_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
if (!hasTransformParameter)
|
|
return result;
|
|
|
|
std::string paramValue = getString(BVH_JOINT_TRANSFORM_STRING_PREFIX + jointName);
|
|
|
|
if (strstr(paramValue.c_str(), "NEGATE_YAW"))
|
|
result |= NEGATE_YAW;
|
|
if (strstr(paramValue.c_str(), "NEGATE_PITCH"))
|
|
result |= NEGATE_PITCH;
|
|
if (strstr(paramValue.c_str(), "NEGATE_ROLL"))
|
|
result |= NEGATE_ROLL;
|
|
if (strstr(paramValue.c_str(), "NEGATE_ALL"))
|
|
return NEGATE_ALL;
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
bool FSFloaterPoser::getWhetherToResetBaseRotationOnEdit() { return gSavedSettings.getBOOL(POSER_RESETBASEROTONEDIT_SAVE_KEY); }
|
|
void FSFloaterPoser::onClickSetBaseRotZero() { mAlsoSaveBvhCbx->setEnabled(getWhetherToResetBaseRotationOnEdit()); }
|
|
|
|
bool FSFloaterPoser::getSavingToBvh()
|
|
{
|
|
return getWhetherToResetBaseRotationOnEdit() && gSavedSettings.getBOOL(POSER_RESETBASEROTONEDIT_SAVE_KEY);
|
|
}
|
|
|