From 08f8af3cb2fc49d06c0baa7fd20ea5efcdffaa30 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Tue, 25 Nov 2025 21:49:26 +0000 Subject: [PATCH] Poser: Update diff save, Rotation frames, minor enhancements Signed-off-by: Angeldark Raymaker No collab poser Manip: Integrates better with Poser, now working 'live' like all the other UI controls, feeding back in the same way Manip: code tidy Rotation framing: World/Avatar/Screen reference framing for manip & other UI elements Bone hightlight: with manip off, a debug marker appears a second to guide the eye as you select bones Diff saves: Better saving of pose-state --- indra/newview/app_settings/settings.xml | 11 + indra/newview/fsfloaterposer.cpp | 344 +++++++++++++-- indra/newview/fsfloaterposer.h | 64 ++- indra/newview/fsjointpose.cpp | 25 +- indra/newview/fsjointpose.h | 37 +- indra/newview/fsmaniprotatejoint.cpp | 414 ++++++++++-------- indra/newview/fsmaniprotatejoint.h | 38 +- indra/newview/fsposeranimator.cpp | 297 +++++++++++-- indra/newview/fsposeranimator.h | 116 +++-- indra/newview/fsposestate.cpp | 275 +++++++++--- indra/newview/fsposestate.h | 101 +++-- indra/newview/fsposingmotion.cpp | 58 ++- indra/newview/fsposingmotion.h | 23 +- indra/newview/lltoolcomp.h | 3 +- .../skins/default/xui/en/floater_fs_poser.xml | 201 ++++++++- 15 files changed, 1538 insertions(+), 469 deletions(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 33788d2d84..0dfddef18c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8177,6 +8177,17 @@ Value 0 + FSPoserShowBoneHighlights + + Comment + Whether to highlight a bone with the debug beacon on selection from the UI. + Persist + 1 + Type + Boolean + Value + 1 + FSPoserStopPosingWhenClosed Comment diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index cd9934dc41..23809f09e9 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -46,6 +46,8 @@ #include "llinventoryfunctions.h" #include "lltoolcomp.h" #include "llloadingindicator.h" +#include "llmutelist.h" +#include "llappviewer.h" namespace { @@ -57,6 +59,7 @@ 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_FRAME_TRANSFORM_PREFIX[] = "joint_frame_"; 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_TRACKPAD_SENSITIVITY_SAVE_KEY = "FSPoserTrackpadSensitivity"; @@ -64,6 +67,7 @@ constexpr std::string_view POSER_STOPPOSINGWHENCLOSED_SAVE_KEY = "FSPoserStopPos constexpr std::string_view POSER_SAVEEXTERNALFORMAT_SAVE_KEY = "FSPoserSaveExternalFileAlso"; constexpr std::string_view POSER_SAVECONFIRMREQUIRED_SAVE_KEY = "FSPoserOnSaveConfirmOverwrite"; constexpr std::string_view POSER_UNLOCKPELVISINBVH_SAVE_KEY = "FSPoserPelvisUnlockedForBvhSave"; +constexpr std::string_view POSER_SHOWBONEHIGHLIGHTS_SAVE_KEY = "FSManipShowJointMarkers"; constexpr char ICON_SAVE_OK[] = "icon_rotation_is_own_work"; constexpr char ICON_SAVE_FAILED[] = "icon_save_failed_button"; @@ -82,6 +86,7 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) 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.ToggleRotationFrame", [this](LLUICtrl* button, const LLSD&) { onToggleRotationFrameButton(button); }); mCommitCallbackRegistrar.add("Poser.UndoLastRotation", [this](LLUICtrl*, const LLSD&) { onUndoLastChange(); }); mCommitCallbackRegistrar.add("Poser.RedoLastRotation", [this](LLUICtrl*, const LLSD&) { onRedoLastChange(); }); @@ -205,9 +210,17 @@ bool FSFloaterPoser::postBuild() mRedoChangeBtn = getChild("button_redo_change"); mUndoChangeBtn = getChild("undo_change"); mSetToTposeButton = getChild("set_t_pose_button"); + mBtnJointReset = getChild("poser_joint_reset"); + + mBtnWorldFrame = getChild("poser_world_frame_toggle"); + mBtnAvatarFrame = getChild("poser_avatar_frame_toggle"); + mBtnScreenFrame = getChild("poser_screen_frame_toggle"); mJointsParentPnl = getChild("joints_parent_panel"); mTrackballPnl = getChild("trackball_panel"); + mPositionPnl = getChild("position_panel"); + mMoveTabPnl = getChild("move_tab_panel"); + mTrackballButtonPnl = getChild("trackball_button_panel"); mPositionRotationPnl = getChild("positionRotation_panel"); mBodyJointsPnl = getChild("body_joints_panel"); mFaceJointsPnl = getChild("face_joints_panel"); @@ -269,6 +282,76 @@ void FSFloaterPoser::onFocusLost() } } +void FSFloaterPoser::draw() +{ + LLFloater::draw(); + + drawOnHoverJointHint(); +} + +void FSFloaterPoser::markSelectedJointsToHighlight() +{ + bool toolsEnabled = mToggleVisualManipulators->getValue().asBoolean(); + if (toolsEnabled) + return; + + bool showHighlights = gSavedSettings.getBOOL(POSER_SHOWBONEHIGHLIGHTS_SAVE_KEY); + if (!showHighlights) + return; + + auto selectedJoints = getUiSelectedPoserJoints(); + if (selectedJoints.size() < 1) + return; + + std::string jointName = selectedJoints[0]->jointName(); + bool isRightLimb = jointName.find("Right") != std::string::npos; + bool isLeftLimb = jointName.find("Left") != std::string::npos; + + if (!(isRightLimb || isLeftLimb)) + return; + + LLVOAvatar* avatar = getUiSelectedAvatar(); + if (!avatar) + return; + + if (!mPoserAnimator.isPosingAvatar(avatar)) + return; + + mLastSelectedJoint = selectedJoints[0]; + timeFadeStartedMicrosec = gFrameTime; +} + +void FSFloaterPoser::drawOnHoverJointHint() +{ + if (!mLastSelectedJoint) + return; + + constexpr U64 GLOW_TIME_US = 300000; + U64 fadeTimeUs = gFrameTime - timeFadeStartedMicrosec; + if (fadeTimeUs > GLOW_TIME_US) + return; + + LLVOAvatar* avatar = getUiSelectedAvatar(); + if (!avatar) + return; + + if (!mPoserAnimator.isPosingAvatar(avatar)) + return; + + LLJoint* joint = avatar->getJoint(std::string(mLastSelectedJoint->jointName())); + if (!joint) + return; + + F32 alphaFade = 1.f * (GLOW_TIME_US - fadeTimeUs) / GLOW_TIME_US; + static LLUIColor mBeaconColor = LLUIColorTable::getInstance()->getColor("AreaSearchBeaconColor"); + LLColor4 beaconColour = mBeaconColor.get(); + beaconColour.setAlpha(alphaFade); + LLVector3 joint_world_position = joint->getWorldPosition(); + + static LLCachedControl beacon_line_width(gSavedSettings, "DebugBeaconLineWidth"); + gObjectList.addDebugBeacon(joint_world_position, "", beaconColour, beaconColour, beacon_line_width); +} + void FSFloaterPoser::enableVisualManipulators() { if (!gAgentAvatarp || gAgentAvatarp.isNull()) @@ -359,6 +442,9 @@ void FSFloaterPoser::onPoseFileSelect() if (!avatar) return; + if (!havePermissionToAnimateAvatar(avatar) && !havePermissionToAnimateOtherAvatar(avatar)) + return; + bool enableButtons = mPoserAnimator.isPosingAvatar(avatar); mLoadPosesBtn->setEnabled(enableButtons); mSavePosesBtn->setEnabled(enableButtons); @@ -409,11 +495,14 @@ void FSFloaterPoser::onClickPoseSave() mSavePosesBtn->setImageOverlay(tryGetString(ICON_SAVE_FAILED), mSavePosesBtn->getImageOverlayHAlign()); return; } - + LLVOAvatar* avatar = getUiSelectedAvatar(); - if (!avatar) - return; - + if (!havePermissionToAnimateAvatar(avatar) && !havePermissionToAnimateOtherAvatar(avatar)) + { + mSavePosesBtn->setImageOverlay(tryGetString(ICON_SAVE_FAILED), mSavePosesBtn->getImageOverlayHAlign()); + return; + } + // if prompts are disabled or file doesn't exist, do the save immediately: const bool prompt = gSavedSettings.getBOOL(POSER_SAVECONFIRMREQUIRED_SAVE_KEY); @@ -534,7 +623,7 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi record["startFromTeePose"]["value"] = !savingDiff; if (savingDiff) - mPoserAnimator.savePosingState(avatar, &record); + mPoserAnimator.savePosingState(avatar, false, &record); LLVector3 rotation, position, scale, zeroVector; bool baseRotationIsZero; @@ -694,17 +783,19 @@ void FSFloaterPoser::onClickRecaptureSelectedBones() if (currentlyPosing) continue; - mPoserAnimator.recaptureJoint(avatar, *item, getJointTranslation(item->jointName()), getJointNegation(item->jointName())); + mPoserAnimator.recaptureJoint(avatar, *item); } setSavePosesButtonText(true); refreshRotationSlidersAndSpinners(); + refreshPositionSlidersAndSpinners(); + refreshScaleSlidersAndSpinners(); refreshTrackpadCursor(); refreshTextHighlightingOnJointScrollLists(); enableOrDisableRedoAndUndoButton(); } -void FSFloaterPoser::updatePosedBones(const std::string& jointName) +void FSFloaterPoser::updatePosedBones(const std::string& jointName, const LLQuaternion rotation, const LLVector3 position, const LLVector3 scale) { LLVOAvatar *avatar = getUiSelectedAvatar(); if (!avatar) @@ -713,12 +804,19 @@ void FSFloaterPoser::updatePosedBones(const std::string& jointName) if (!mPoserAnimator.isPosingAvatar(avatar)) return; + bool haveImplicitPermission = havePermissionToAnimateAvatar(avatar); // self & control avatars you own + bool iCanPoseThem = havePermissionToAnimateOtherAvatar(avatar); + if (!haveImplicitPermission && !iCanPoseThem) + return; + const FSPoserAnimator::FSPoserJoint* poserJoint = mPoserAnimator.getPoserJointByName(jointName); if (!poserJoint) return; - bool savingToExternal = getSavingToBvh(); - mPoserAnimator.recaptureJointAsDelta(avatar, poserJoint, savingToExternal, getUiSelectedBoneDeflectionStyle()); + bool savingToExternal = getSavingToBvh(); + E_PoserReferenceFrame frame = getReferenceFrame(); + E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle(); + mPoserAnimator.updateJointFromManip(avatar, poserJoint, savingToExternal, defl, frame, rotation, position, scale); refreshRotationSlidersAndSpinners(); refreshPositionSlidersAndSpinners(); @@ -728,6 +826,24 @@ void FSFloaterPoser::updatePosedBones(const std::string& jointName) refreshTextHighlightingOnJointScrollLists(); } +LLQuaternion FSFloaterPoser::getManipGimbalRotation(const std::string& jointName) +{ + LLVOAvatar *avatar = getUiSelectedAvatar(); + if (!avatar) + return LLQuaternion(); + + if (!mPoserAnimator.isPosingAvatar(avatar)) + return LLQuaternion(); + + const FSPoserAnimator::FSPoserJoint* poserJoint = mPoserAnimator.getPoserJointByName(jointName); + if (!poserJoint) + return LLQuaternion(); + + E_PoserReferenceFrame frame = getReferenceFrame(); + + return mPoserAnimator.getManipGimbalRotation(avatar, poserJoint, frame); +} + void FSFloaterPoser::onClickBrowsePoseCache() { createUserPoseDirectoryIfNeeded(); @@ -930,6 +1046,9 @@ void FSFloaterPoser::timedReload() if (!avatar) return; + if (!havePermissionToAnimateAvatar(avatar) && !havePermissionToAnimateOtherAvatar(avatar)) + return; + if (loadPoseFromXml(avatar, mLoadPoseTimer->getPosePath(), mLoadPoseTimer->getLoadMethod())) { mLoadPoseTimer->completeLoading(); @@ -1024,6 +1143,8 @@ void FSFloaterPoser::onClickLoadHandPose(bool isRightHand) LLVOAvatar* avatar = getUiSelectedAvatar(); if (!avatar) return; + if (!havePermissionToAnimateAvatar(avatar) && !havePermissionToAnimateOtherAvatar(avatar)) + return; if (!mPoserAnimator.isPosingAvatar(avatar)) return; @@ -1233,8 +1354,8 @@ bool FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose mPoserAnimator.setRotationIsMirrored(avatar, *poserJoint, mirroredJoint); } - if (version > 6 && !startFromZeroRot) - loadSuccess = mPoserAnimator.loadPosingState(avatar, pose); + if (version > 6 && !startFromZeroRot && !loadSelective) + loadSuccess = mPoserAnimator.loadPosingState(avatar, true, pose); } } catch ( const std::exception & e ) @@ -1326,8 +1447,25 @@ bool FSFloaterPoser::havePermissionToAnimateAvatar(LLVOAvatar *avatar) const return false; if (avatar->isSelf()) return true; + if (avatar->isControlAvatar()) - return true; + { + LLControlAvatar* control_av = dynamic_cast(avatar); + const LLVOVolume* rootVolume = control_av->mRootVolp; + const LLViewerObject* rootEditObject = (rootVolume) ? rootVolume->getRootEdit() : NULL; + if (!rootEditObject) + return false; + + return rootEditObject->permYouOwner(); + } + + return false; +} + +bool FSFloaterPoser::havePermissionToAnimateOtherAvatar(LLVOAvatar *avatar) const +{ + if (!avatar || avatar->isDead()) + return false; return false; } @@ -1342,6 +1480,12 @@ void FSFloaterPoser::poseControlsEnable(bool enable) mLoadPosesBtn->setEnabled(enable); mSavePosesBtn->setEnabled(enable); mPoseSaveNameEditor->setEnabled(enable); + mBtnJointReset->setEnabled(enable); + mRedoChangeBtn->setEnabled(enable); + mUndoChangeBtn->setEnabled(enable); + mPositionPnl->setEnabled(enable); + mMoveTabPnl->setEnabled(enable); + mTrackballButtonPnl->setEnabled(enable); } void FSFloaterPoser::refreshJointScrollListMembers() @@ -1500,6 +1644,23 @@ void FSFloaterPoser::setRotationChangeButtons(bool togglingMirror, bool toggling refreshTrackpadCursor(); } +void FSFloaterPoser::onToggleRotationFrameButton(const LLUICtrl* toggleButton) +{ + if (!toggleButton) + return; + + bool buttonDown = toggleButton->getValue().asBoolean(); + if (buttonDown) + { + mBtnAvatarFrame->setValue(toggleButton == mBtnAvatarFrame); + mBtnScreenFrame->setValue(toggleButton == mBtnScreenFrame); + mBtnWorldFrame->setValue(toggleButton == mBtnWorldFrame); + } + + FSToolCompPose::getInstance()->setReferenceFrame(getReferenceFrame()); + refreshRotationSlidersAndSpinners(); +} + void FSFloaterPoser::onUndoLastChange() { LLVOAvatar* avatar = getUiSelectedAvatar(); @@ -1733,7 +1894,6 @@ LLScrollListCtrl* FSFloaterPoser::getScrollListForTab(LLPanel * tabPanel) const return mCollisionVolumesScrollList; } - LL_WARNS() << "Unknown tab panel: " << tabPanel << LL_ENDL; return nullptr; } @@ -1787,7 +1947,10 @@ void FSFloaterPoser::updateManipWithFirstSelectedJoint(std::vector= 1) + bool haveImplicitPermission = havePermissionToAnimateAvatar(avatarp); + bool iCanPoseThem = havePermissionToAnimateOtherAvatar(avatarp); + + if ((joints.size() >= 1) && (haveImplicitPermission || iCanPoseThem)) FSToolCompPose::getInstance()->setJoint(avatarp->getJoint(joints[0]->jointName())); else FSToolCompPose::getInstance()->setJoint(nullptr); @@ -2073,8 +2236,9 @@ void FSFloaterPoser::setSelectedJointsPosition(F32 x, F32 y, F32 z) if (!mPoserAnimator.isPosingAvatar(avatar)) return; - LLVector3 vec3 = LLVector3(x, y, z); - E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle(); + LLVector3 vec3 = LLVector3(x, y, z); + E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle(); + E_PoserReferenceFrame frame = getReferenceFrame(); for (auto item : getUiSelectedPoserJoints()) { @@ -2082,7 +2246,7 @@ void FSFloaterPoser::setSelectedJointsPosition(F32 x, F32 y, F32 z) if (!currentlyPosingJoint) continue; - mPoserAnimator.setJointPosition(avatar, item, vec3, defl); + mPoserAnimator.setJointPosition(avatar, item, vec3, frame, defl); } } @@ -2097,7 +2261,8 @@ void FSFloaterPoser::setSelectedJointsRotation(const LLVector3& absoluteRot, con auto selectedJoints = getUiSelectedPoserJoints(); bool savingToExternal = getSavingToBvh(); - E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle(); + E_BoneDeflectionStyles deflection = getUiSelectedBoneDeflectionStyle(); + E_PoserReferenceFrame frame = getReferenceFrame(); for (auto item : selectedJoints) { @@ -2111,14 +2276,17 @@ void FSFloaterPoser::setSelectedJointsRotation(const LLVector3& absoluteRot, con bool oppositeJointAlsoSelectedOnUi = std::find(selectedJoints.begin(), selectedJoints.end(), oppositeJoint) != selectedJoints.end(); - bool deflectionDoesOppositeLimbs = !(defl == NONE || defl == DELTAMODE); + bool deflectionDoesOppositeLimbs = !(deflection == NONE || deflection == DELTAMODE); if (oppositeJointAlsoSelectedOnUi && deflectionDoesOppositeLimbs && item->dontFlipOnMirror()) continue; } - mPoserAnimator.setJointRotation(avatar, item, absoluteRot, deltaRot, defl, - getJointTranslation(item->jointName()), getJointNegation(item->jointName()), savingToExternal, - getUiSelectedBoneRotationStyle(item->jointName())); + S32 jointNegation = getJointNegation(frame, item->jointName()); + E_BoneAxisTranslation translation = getJointTranslation(frame, item->jointName()); + E_RotationStyle style = getUiSelectedBoneRotationStyle(item->jointName()); + + mPoserAnimator.setJointRotation(avatar, item, absoluteRot, deltaRot, deflection, frame, translation, jointNegation, + savingToExternal, style); } if (savingToExternal) @@ -2134,8 +2302,9 @@ void FSFloaterPoser::setSelectedJointsScale(F32 x, F32 y, F32 z) if (!mPoserAnimator.isPosingAvatar(avatar)) return; - LLVector3 vec3 = LLVector3(x, y, z); - E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle(); + LLVector3 vec3 = LLVector3(x, y, z); + E_BoneDeflectionStyles defl = getUiSelectedBoneDeflectionStyle(); + E_PoserReferenceFrame frame = getReferenceFrame(); for (auto item : getUiSelectedPoserJoints()) { @@ -2143,7 +2312,7 @@ void FSFloaterPoser::setSelectedJointsScale(F32 x, F32 y, F32 z) if (!currentlyPosingJoint) continue; - mPoserAnimator.setJointScale(avatar, item, vec3, defl); + mPoserAnimator.setJointScale(avatar, item, vec3, frame, defl); } } @@ -2161,8 +2330,10 @@ LLVector3 FSFloaterPoser::getRotationOfFirstSelectedJoint() const if (!mPoserAnimator.isPosingAvatar(avatar)) return rotation; - rotation = mPoserAnimator.getJointRotation(avatar, *selectedJoints.front(), getJointTranslation(selectedJoints.front()->jointName()), - getJointNegation(selectedJoints.front()->jointName())); + E_PoserReferenceFrame frame = getReferenceFrame(); + + rotation = mPoserAnimator.getJointRotation(avatar, *selectedJoints.front(), getJointTranslation(frame, selectedJoints.front()->jointName()), + getJointNegation(frame, selectedJoints.front()->jointName())); return rotation; } @@ -2210,18 +2381,25 @@ void FSFloaterPoser::onJointTabSelect() refreshTrackpadCursor(); enableOrDisableRedoAndUndoButton(); refreshScaleSlidersAndSpinners(); + markSelectedJointsToHighlight(); } -E_BoneAxisTranslation FSFloaterPoser::getJointTranslation(const std::string& jointName) const +E_BoneAxisTranslation FSFloaterPoser::getJointTranslation(E_PoserReferenceFrame frame, const std::string& jointName) const { if (jointName.empty()) return SWAP_NOTHING; - bool hasTransformParameter = hasString(XML_JOINT_TRANSFORM_STRING_PREFIX + jointName); + std::string paramName; + if (frame == POSER_FRAME_BONE) + paramName = XML_JOINT_TRANSFORM_STRING_PREFIX + jointName; + else + paramName = XML_JOINT_FRAME_TRANSFORM_PREFIX + jointName; + + bool hasTransformParameter = hasString(paramName); if (!hasTransformParameter) return SWAP_NOTHING; - std::string paramValue = getString(XML_JOINT_TRANSFORM_STRING_PREFIX + jointName); + std::string paramValue = getString(paramName); if (strstr(paramValue.c_str(), "SWAP_YAW_AND_ROLL")) return SWAP_YAW_AND_ROLL; @@ -2237,18 +2415,24 @@ E_BoneAxisTranslation FSFloaterPoser::getJointTranslation(const std::string& joi return SWAP_NOTHING; } -S32 FSFloaterPoser::getJointNegation(const std::string& jointName) const +S32 FSFloaterPoser::getJointNegation(E_PoserReferenceFrame frame, 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) + std::string paramName; + if (frame == POSER_FRAME_BONE) + paramName = XML_JOINT_TRANSFORM_STRING_PREFIX + jointName; + else + paramName = XML_JOINT_FRAME_TRANSFORM_PREFIX + jointName; + + bool hasNegationParameter = hasString(paramName); + if (!hasNegationParameter) return result; - std::string paramValue = getString(XML_JOINT_TRANSFORM_STRING_PREFIX + jointName); + std::string paramValue = getString(paramName); if (strstr(paramValue.c_str(), "NEGATE_YAW")) result |= NEGATE_YAW; @@ -2262,6 +2446,23 @@ S32 FSFloaterPoser::getJointNegation(const std::string& jointName) const return result; } +E_PoserReferenceFrame FSFloaterPoser::getReferenceFrame() const +{ + bool toggleButtonValue = mBtnScreenFrame->getValue().asBoolean(); + if (toggleButtonValue) + return POSER_FRAME_CAMERA; + + toggleButtonValue = mBtnAvatarFrame->getValue().asBoolean(); + if (toggleButtonValue) + return POSER_FRAME_AVATAR; + + toggleButtonValue = mBtnWorldFrame->getValue().asBoolean(); + if (toggleButtonValue) + return POSER_FRAME_WORLD; + + return POSER_FRAME_BONE; +} + /// /// 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. @@ -2269,16 +2470,26 @@ S32 FSFloaterPoser::getJointNegation(const std::string& jointName) const void FSFloaterPoser::onAvatarSelect() { LLVOAvatar* avatar = getUiSelectedAvatar(); - if(avatar) - { + if (!avatar) + return; + + bool isSelf = avatar->isSelf(); + bool haveImplicitPermission = havePermissionToAnimateAvatar(avatar); // self & control avatars you own + bool haveExplicitPermission = havePermissionToAnimateOtherAvatar(avatar); // as permissions allow + + if (haveImplicitPermission || haveExplicitPermission) FSToolCompPose::getInstance()->setAvatar(avatar); - } - mStartStopPosingBtn->setEnabled(couldAnimateAvatar(avatar)); + else + FSToolCompPose::getInstance()->setAvatar(nullptr); bool arePosingSelected = mPoserAnimator.isPosingAvatar(avatar); + + mStartStopPosingBtn->setEnabled(haveImplicitPermission); mStartStopPosingBtn->setValue(arePosingSelected); - mSetToTposeButton->setEnabled(arePosingSelected); - poseControlsEnable(arePosingSelected); + + mSetToTposeButton->setEnabled(haveImplicitPermission && arePosingSelected); + poseControlsEnable(arePosingSelected && haveImplicitPermission); + refreshTextHighlightingOnAvatarScrollList(); refreshTextHighlightingOnJointScrollLists(); onJointTabSelect(); @@ -2292,7 +2503,16 @@ uuid_vec_t FSFloaterPoser::getNearbyAvatarsAndAnimeshes() const for (LLCharacter* character : LLCharacter::sInstances) { LLVOAvatar* avatar = dynamic_cast(character); - if (!havePermissionToAnimateAvatar(avatar)) + if (!avatarIsNearbyMe(avatar)) + continue; + + bool isMuted = LLMuteList::getInstance()->isMuted(avatar->getID()); + if (isMuted) + continue; + + bool isSelfOrCtrl = avatar->isControlAvatar() || avatar->isSelf(); + + if (!isSelfOrCtrl) continue; avatar_ids.emplace_back(character->getID()); @@ -2301,6 +2521,16 @@ uuid_vec_t FSFloaterPoser::getNearbyAvatarsAndAnimeshes() const return avatar_ids; } +bool FSFloaterPoser::avatarIsNearbyMe(LLCharacter* character) const +{ + if (!gAgentAvatarp || gAgentAvatarp.isNull() || !character) + return false; + + LLVector3 separationVector = character->getCharacterPosition() - gAgentAvatarp->getCharacterPosition(); + + return separationVector.magVec() < 50.f; +} + uuid_vec_t FSFloaterPoser::getCurrentlyListedAvatarsAndAnimeshes() const { uuid_vec_t avatar_ids; @@ -2351,10 +2581,6 @@ void FSFloaterPoser::onAvatarsRefresh() 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"); @@ -2377,10 +2603,17 @@ void FSFloaterPoser::onAvatarsRefresh() if (!LLAvatarNameCache::get(uuid, &av_name)) continue; + bool isMuted = LLMuteList::getInstance()->isMuted(uuid); + if (isMuted) + continue; + + if (!avatar->isSelf()) + continue; + LLSD row; row["columns"][COL_ICON]["column"] = "icon"; row["columns"][COL_ICON]["type"] = "icon"; - row["columns"][COL_ICON]["value"] = iconCatagoryName; + row["columns"][COL_ICON]["value"] = getIconNameForAvatar(avatar); row["columns"][COL_NAME]["column"] = "name"; row["columns"][COL_NAME]["value"] = av_name.getDisplayName(); row["columns"][COL_UUID]["column"] = "uuid"; @@ -2423,6 +2656,21 @@ void FSFloaterPoser::onAvatarsRefresh() refreshTextHighlightingOnAvatarScrollList(); } +std::string FSFloaterPoser::getIconNameForAvatar(LLVOAvatar* avatar) +{ + std::string iconName = "Inv_BodyShape"; + if (hasString("icon_category")) + iconName = getString("icon_category"); + + if (!avatar) + return iconName; + + if (avatar->isControlAvatar() && hasString("icon_object")) + return getString("icon_object"); + + return iconName; +} + std::string FSFloaterPoser::getControlAvatarName(const LLControlAvatar* avatar) { if (!avatar) @@ -2456,6 +2704,8 @@ void FSFloaterPoser::refreshTextHighlightingOnAvatarScrollList() LLUUID selectedAvatarId = cell->getValue().asUUID(); LLVOAvatar* listAvatar = getAvatarByUuid(selectedAvatarId); + ((LLScrollListText*)listItem->getColumn(COL_ICON))->setValue(getIconNameForAvatar(listAvatar)); + if (mPoserAnimator.isPosingAvatar(listAvatar)) ((LLScrollListText *) listItem->getColumn(COL_NAME))->setFontStyle(LLFontGL::BOLD); else @@ -2530,7 +2780,7 @@ std::string FSFloaterPoser::getScrollListIconForJoint(LLVOAvatar* avatar, FSPose return tryGetString("icon_rotation_bvh_unlocked"); } -std::string FSFloaterPoser::tryGetString(std::string name) +std::string FSFloaterPoser::tryGetString(std::string_view name) { if (name.empty()) return ""; diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 8d289f53f7..645bc9de3f 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -31,6 +31,7 @@ #include "llfloater.h" #include "lltoolmgr.h" #include "fsposeranimator.h" +#include "fsmaniprotatejoint.h" class FSVirtualTrackpad; class LLButton; @@ -80,7 +81,8 @@ class FSFloaterPoser : public LLFloater, public LLEditMenuHandler friend class LLFloaterReg; FSFloaterPoser(const LLSD &key); public: - void updatePosedBones(const std::string& jointName); + void updatePosedBones(const std::string& jointName, const LLQuaternion rotation, const LLVector3 position, const LLVector3 scale); + LLQuaternion getManipGimbalRotation(const std::string& jointName); void selectJointByName(const std::string& jointName); void undo() override { onUndoLastChange(); }; bool canUndo() const override { return true; } @@ -94,6 +96,8 @@ public: void onClose(bool app_quitting) override; void onFocusReceived() override; void onFocusLost() override; + virtual void draw() override; + /// /// Refreshes the supplied pose list from the supplued subdirectory. /// @@ -181,6 +185,13 @@ public: /// A the collection of UUIDs for nearby avatars. uuid_vec_t getNearbyAvatarsAndAnimeshes() const; + /// + /// Gets whether the supplied character is within chat range of gAgentAvatar. + /// + /// The character to query whether nearby. + /// True if the supplied character is within chat range, otherwise false. + bool avatarIsNearbyMe(LLCharacter* character) const; + /// /// Gets a collection of UUIDs for avatars currently being presented on the UI. /// @@ -242,12 +253,17 @@ public: void enableVisualManipulators(); void disableVisualManipulators(); + // Visual cue for which bone is under the mouse-cursor + void drawOnHoverJointHint(); + void markSelectedJointsToHighlight(); + // UI Event Handlers void onAvatarsRefresh(); void onAvatarSelect(); void onJointTabSelect(); void onToggleMirrorChange(); void onToggleSympatheticChange(); + void onToggleRotationFrameButton(const LLUICtrl* toggleButton); void onToggleVisualManipulators(); void setRotationChangeButtons(bool mirror, bool sympathetic); void onUndoLastChange(); @@ -280,6 +296,14 @@ public: void refreshTrackpadCursor(); void enableOrDisableRedoAndUndoButton(); + + /// + /// Determines if we have permission to animate the supplied avatar. + /// + /// The avatar to animate. + /// True if we have permission to animate, otherwise false. + bool havePermissionToAnimateOtherAvatar(LLVOAvatar* avatar) const; + /// /// Determines if we have permission to animate the supplied avatar. /// @@ -306,6 +330,7 @@ public: /// This facilitates 'conceptual' conversion of Euler frame to up/down, left/right and roll and is rather subjective. /// Thus, many of these 'conversions' are backed by values in the XML. /// + /// The reference frame for the change. /// The well-known name of the joint, eg: mChest. /// The axial translation so the oily angles make better sense in terms of up/down/left/right/roll. /// @@ -313,14 +338,21 @@ public: /// No the translation isn't untangling all of that, it's not needed until it is. /// We're not landing on Mars with this code, just offering a user reasonable thumb-twiddlings. /// - E_BoneAxisTranslation getJointTranslation(const std::string& jointName) const; + E_BoneAxisTranslation getJointTranslation(E_PoserReferenceFrame frame, const std::string& jointName) const; /// /// Gets the collection of E_BoneAxisNegation values for the supplied joint. /// + /// The reference frame for the change. /// The name of the joind to get the axis transformation for. /// The kind of axis transformation to perform. - S32 getJointNegation(const std::string& jointName) const; + S32 getJointNegation(E_PoserReferenceFrame frame, const std::string& jointName) const; + + /// + /// Gets the reference frame for the rotation/position/scale change. + /// + /// The reference frame for the change. + E_PoserReferenceFrame getReferenceFrame() const; /// /// Gets the axial translation required for joints when saving to BVH. @@ -336,6 +368,13 @@ public: /// void refreshTextHighlightingOnAvatarScrollList(); + /// + /// Gets an appropriate icon for the supplied avatar, based on sharing permission. + /// + /// The avatar to get an icon for. + /// A string with the name of an icon. + std::string getIconNameForAvatar(LLVOAvatar* avatar); + /// /// Refreshes the text on all joints scroll lists based on their state. /// @@ -367,7 +406,7 @@ public: /// /// The name of the string. /// The named string, if it exists, otherwise an empty string. - std::string tryGetString(std::string name); + std::string tryGetString(std::string_view name); /// /// Gets the name of an item from the supplied object ID. @@ -450,10 +489,12 @@ public: /// static F32 clipRange(F32 value); - LLToolset* mLastToolset{ nullptr }; - LLTool* mJointRotTool{ nullptr }; - - LLVector3 mLastSliderRotation; + LLToolset* mLastToolset{ nullptr }; + LLTool* mJointRotTool{ nullptr }; + + LLVector3 mLastSliderRotation; + FSPoserAnimator::FSPoserJoint* mLastSelectedJoint{ nullptr }; + U64 timeFadeStartedMicrosec; FSVirtualTrackpad* mAvatarTrackball{ nullptr }; @@ -502,6 +543,10 @@ public: LLButton* mUndoChangeBtn{ nullptr }; LLButton* mSetToTposeButton{ nullptr }; LLButton* mBtnJointRotate{ nullptr }; + LLButton* mBtnJointReset{ nullptr }; + LLButton* mBtnWorldFrame{ nullptr }; + LLButton* mBtnAvatarFrame{ nullptr }; + LLButton* mBtnScreenFrame{ nullptr }; LLLineEditor* mPoseSaveNameEditor{ nullptr }; @@ -516,6 +561,9 @@ public: LLPanel* mMiscJointsPnl{ nullptr }; LLPanel* mCollisionVolumesPnl{ nullptr }; LLPanel* mPosesLoadSavePnl{ nullptr }; + LLPanel* mPositionPnl{ nullptr }; + LLPanel* mMoveTabPnl{ nullptr }; + LLPanel* mTrackballButtonPnl{ nullptr }; LLCheckBoxCtrl* mAlsoSaveBvhCbx{ nullptr }; LLCheckBoxCtrl* mUnlockPelvisInBvhSaveCbx{ nullptr }; diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index b0e879545e..e48500eef0 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -47,6 +47,7 @@ FSJointPose::FSJointPose(LLJoint* joint, U32 usage, bool isCollisionVolume) mJointName = joint->getName(); mIsCollisionVolume = isCollisionVolume; + mJointNumber = joint->getJointNum(); mCurrentState = FSJointState(joint); } @@ -64,6 +65,8 @@ void FSJointPose::setPublicRotation(bool zeroBase, const LLQuaternion& rot) if (zeroBase) zeroBaseRotation(true); + else + mCurrentState.mUserSpecifiedBaseZero = false; mCurrentState.mRotation.set(rot); mCurrentState.mLastChangeWasRotational = true; @@ -98,6 +101,8 @@ void FSJointPose::resetJoint() void FSJointPose::addStateToUndo(FSJointState stateToAddToUndo) { + mModifiedThisSession = true; + auto timeIntervalSinceLastChange = std::chrono::system_clock::now() - mTimeLastUpdatedCurrentState; mTimeLastUpdatedCurrentState = std::chrono::system_clock::now(); @@ -151,27 +156,28 @@ FSJointPose::FSJointState FSJointPose::redoLastStateChange(FSJointState thingToS void FSJointPose::recaptureJoint() { - if (mIsCollisionVolume) - return; - LLJoint* joint = mJointState->getJoint(); if (!joint) return; addStateToUndo(FSJointState(mCurrentState)); + + if (mIsCollisionVolume) + { + mCurrentState.mPosition.set(LLVector3::zero); + mCurrentState.mScale.set(LLVector3::zero); + } + mCurrentState = FSJointState(joint); mCurrentState.mLastChangeWasRotational = true; } -LLQuaternion FSJointPose::recaptureJointAsDelta(bool zeroBase) +LLQuaternion FSJointPose::updateJointAsDelta(bool zeroBase, const LLQuaternion rotation, const LLVector3 position, const LLVector3 scale) { - LLJoint* joint = mJointState->getJoint(); - if (!joint) - return LLQuaternion::DEFAULT; - addStateToUndo(FSJointState(mCurrentState)); mCurrentState.mLastChangeWasRotational = true; - return mCurrentState.updateFromJoint(joint, zeroBase); + + return mCurrentState.updateFromJointProperties(zeroBase, rotation, position, scale); } void FSJointPose::setBaseRotation(LLQuaternion rotation, LLJoint::JointPriority priority) @@ -247,6 +253,7 @@ void FSJointPose::reflectRotation() if (mIsCollisionVolume) return; + mModifiedThisSession = true; mCurrentState.reflectRotation(); } diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index f87856795f..003aa28d36 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -97,6 +97,8 @@ class FSJointPose /// '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. + /// If zeroBase is true, we treat rotations as if in BVH mode: user work. + /// If zeroBase is false, we treat as NOT BVH: some existing pose and user work. /// void setPublicRotation(bool zeroBase, const LLQuaternion& rot); @@ -173,8 +175,11 @@ class FSJointPose /// Recalculates the delta reltive to the base for a new rotation. /// /// Whether to zero the base rotation on setting the supplied rotation. + /// The rotation of the supplied joint. + /// The position of the supplied joint. + /// The scale of the supplied joint. /// The rotation of the public difference between before and after recapture. - LLQuaternion recaptureJointAsDelta(bool zeroBase); + LLQuaternion updateJointAsDelta(bool zeroBase, const LLQuaternion rotation, const LLVector3 position, const LLVector3 scale); /// /// Sets the base rotation to the supplied rotation if the supplied priority is appropriate. @@ -253,6 +258,18 @@ class FSJointPose /// LLPointer getJointState() const { return mJointState; } + /// + /// Gets whether this joint has been modified this session. + /// + /// True if the joint has been changed at all, otherwise false. + bool getJointModified() const { return mModifiedThisSession; } + + /// + /// Gets the number of the joint represented by this. + /// + /// The joint number, derived from LLjoint. + S32 getJointNumber() const { return mJointNumber; } + class FSJointState { public: @@ -326,15 +343,12 @@ class FSJointPose joint->setScale(mBaseScale); } - LLQuaternion updateFromJoint(LLJoint* joint, bool zeroBase) + LLQuaternion updateFromJointProperties(bool zeroBase, const LLQuaternion rotation, const LLVector3 position, const LLVector3 scale) { - if (!joint) - return LLQuaternion::DEFAULT; - LLQuaternion initalPublicRot = mRotation; LLQuaternion invRot = mBaseRotation; invRot.conjugate(); - LLQuaternion newPublicRot = joint->getRotation() * invRot; + LLQuaternion newPublicRot = rotation * invRot; if (zeroBase) { @@ -343,8 +357,8 @@ class FSJointPose } mRotation.set(newPublicRot); - mPosition.set(joint->getPosition() - mBasePosition); - mScale.set(joint->getScale() - mBaseScale); + mPosition.set(position - mBasePosition); + mScale.set(scale - mBaseScale); return newPublicRot *= ~initalPublicRot; } @@ -450,6 +464,13 @@ class FSJointPose /// bool mIsCollisionVolume{ false }; + S32 mJointNumber = -1; + + /// + /// Whether this joint has ever been changed by poser. + /// + bool mModifiedThisSession{ false }; + std::deque mLastSetJointStates; size_t mUndoneJointStatesIndex = 0; std::chrono::system_clock::time_point mTimeLastUpdatedCurrentState = std::chrono::system_clock::now(); diff --git a/indra/newview/fsmaniprotatejoint.cpp b/indra/newview/fsmaniprotatejoint.cpp index 1cdd3d47b4..41ca92a1ca 100644 --- a/indra/newview/fsmaniprotatejoint.cpp +++ b/indra/newview/fsmaniprotatejoint.cpp @@ -25,11 +25,8 @@ * $/LicenseInfo$ */ - #include "llviewerprecompiledheaders.h" - - #include "fsmaniprotatejoint.h" - -// library includes +#include "llviewerprecompiledheaders.h" +#include "fsmaniprotatejoint.h" #include "llmath.h" #include "llgl.h" #include "llrender.h" @@ -37,11 +34,10 @@ #include "llprimitive.h" #include "llview.h" #include "llfontgl.h" - #include "llrendersphere.h" #include "llvoavatar.h" #include "lljoint.h" -#include "llagent.h" // for gAgent, etc. +#include "llagent.h" // for gAgent, etc. #include "llagentcamera.h" #include "llappviewer.h" #include "llcontrol.h" @@ -173,15 +169,18 @@ static void renderStaticSphere(const LLVector3& joint_world_position, const LLCo } } - bool FSManipRotateJoint::isMouseOverJoint(S32 mouseX, S32 mouseY, const LLVector3& jointWorldPos, F32 jointRadius, F32& outDistanceFromCamera, F32& outRayDistanceFromCenter) const { - // LL_INFOS("FSManipRotateJoint") << "Checking mouse("<< mouseX << "," << mouseY << ") over joint at: " << jointWorldPos << LL_ENDL; - - auto joint_center = gAgent.getPosGlobalFromAgent( jointWorldPos ); + if (!mJoint || !mAvatar) + return false; + + if (mAvatar->isDead() || !mAvatar->isFullyLoaded()) + return false; + + auto joint_center = mAvatar->getPosGlobalFromAgent(jointWorldPos); // centre in *agent* space - LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(joint_center); + LLVector3 agent_space_center = mAvatar->getPosAgentFromGlobal(joint_center); LLVector3 ray_pt, ray_dir; LLManipRotate::mouseToRay(mouseX, mouseY, &ray_pt, &ray_dir); @@ -201,9 +200,9 @@ bool FSManipRotateJoint::isMouseOverJoint(S32 mouseX, S32 mouseY, const LLVector outDistanceFromCamera = proj_len - offset; // distance along the ray to the front intersection outRayDistanceFromCenter = offset; return true; - } + } } - return (false); + return false; } //static @@ -232,7 +231,6 @@ const std::vector FSManipRotateJoint::sSelectableJoints = { "mHipRight" }, { "mKneeRight" }, { "mAnkleRight" }, - }; const std::unordered_map FSManipRotateJoint::sRingParams = @@ -241,6 +239,7 @@ const std::unordered_mapgetEnd(); @@ -331,13 +330,14 @@ void FSManipRotateJoint::highlightHoverSpheres(S32 mouseX, S32 mouseY) mHighlightedJoint = nullptr; // reset the highlighted joint // Iterate through the avatar's joint map. - F32 nearest_hit_distance = 0.f; - F32 nearest_ray_distance = 0.f; - LLJoint * nearest_joint = nullptr; - for ( const auto& entry : getSelectableJoints()) + F32 nearest_hit_distance = 0.f; + F32 nearest_ray_distance = 0.f; + LLJoint* nearest_joint = nullptr; + LLCachedControl target_radius(gSavedSettings, "FSManipRotateJointTargetSize", 0.03f); + + for (const auto& entry : getSelectableJoints()) { - - LLJoint* joint = mAvatar->getJoint(std::string(entry)); + LLJoint* joint = mAvatar->getJoint(std::string(entry)); if (!joint) continue; @@ -347,23 +347,22 @@ void FSManipRotateJoint::highlightHoverSpheres(S32 mouseX, S32 mouseY) // Retrieve the joint's world position (in agent space). LLVector3 jointWorldPos = joint->getWorldPosition(); - LLCachedControl target_radius(gSavedSettings, "FSManipRotateJointTargetSize", 0.03f); F32 distance_from_camera; F32 distance_from_joint; - if (isMouseOverJoint(mouseX, mouseY, jointWorldPos, target_radius, distance_from_camera, distance_from_joint) == true) + if (!isMouseOverJoint(mouseX, mouseY, jointWorldPos, target_radius, distance_from_camera, distance_from_joint)) + continue; + + // we want to highlight the closest + // If there is no joint or this joint is a closer hit than the previous one + if (!nearest_joint || nearest_ray_distance > distance_from_camera || + (nearest_ray_distance == distance_from_camera && nearest_hit_distance > distance_from_joint)) { - // we want to highlight the closest - // If there is no joint or - // this joint is a closer hit than the previous one - if (!nearest_joint || nearest_ray_distance > distance_from_camera || - (nearest_ray_distance == distance_from_camera && nearest_hit_distance > distance_from_joint)) - { - nearest_joint = joint; - nearest_hit_distance = distance_from_joint; - nearest_ray_distance = distance_from_camera; - } + nearest_joint = joint; + nearest_hit_distance = distance_from_joint; + nearest_ray_distance = distance_from_camera; } } + mHighlightedJoint = nearest_joint; } @@ -376,19 +375,26 @@ FSManipRotateJoint::FSManipRotateJoint(LLToolComposite* composite) void FSManipRotateJoint::setJoint(LLJoint* joint) { mJoint = joint; + if (!mJoint) + return; // Save initial rotation as baseline for delta rotation - if (mJoint) - { - mSavedJointRot = mJoint->getWorldRotation(); - mBoneAxes = computeBoneAxes(); - mNaturalAlignmentQuat = computeAlignmentQuat(mBoneAxes); - } + mSavedJointRot = getSelectedJointWorldRotation(); + mBoneAxes = computeBoneAxes(); + mNaturalAlignmentQuat = computeAlignmentQuat(mBoneAxes); } void FSManipRotateJoint::setAvatar(LLVOAvatar* avatar) { mAvatar = avatar; + + if (!avatar) + mJoint = nullptr; + + if (!mJoint) + return; + + setJoint(avatar->getJoint(mJoint->getJointNum())); } /** @@ -403,7 +409,7 @@ void FSManipRotateJoint::handleSelect() // Not entirely sure this is needed in the current implementation. if (mJoint) { - mSavedJointRot = mJoint->getWorldRotation(); + mSavedJointRot = getSelectedJointWorldRotation(); } } @@ -422,15 +428,12 @@ void FSManipRotateJoint::handleSelect() */ bool FSManipRotateJoint::updateVisiblity() { - if (!mJoint) - { - // No joint to manipulate, not visible + if (!isAvatarJointSafeToUse()) return false; - } if (!hasMouseCapture()) { - mRotationCenter = gAgent.getPosGlobalFromAgent( mJoint->getWorldPosition() ); + mRotationCenter = mAvatar->getPosGlobalFromAgent(mJoint->getWorldPosition()); mCamEdgeOn = false; } @@ -439,7 +442,7 @@ bool FSManipRotateJoint::updateVisiblity() //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 LLVector3 agent_space_center = mAvatar->getPosAgentFromGlobal(mRotationCenter); // Convert from world/agent to global const auto * viewer_camera = LLViewerCamera::getInstance(); visible = viewer_camera->projectPosAgentToScreen(agent_space_center, mCenterScreen ); @@ -473,7 +476,6 @@ bool FSManipRotateJoint::updateVisiblity() return visible; } - /** * @brief Updates the scale of a specific manipulator part. * @@ -500,21 +502,29 @@ void FSManipRotateJoint::renderRingPass(const RingRenderParams& params, float ra { 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]); - } + // 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; + 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); @@ -552,7 +562,7 @@ void FSManipRotateJoint::renderManipulatorRings(const LLVector3& agent_space_cen for (int pass = 0; pass < 2; ++pass) { - if( mManipPart == LL_NO_PART || mManipPart == LL_ROT_ROLL || mHighlightedPart == LL_ROT_ROLL) + if (mManipPart == LL_NO_PART || mManipPart == LL_ROT_ROLL || mHighlightedPart == LL_ROT_ROLL) { renderCenterSphere( mRadiusMeters); for (auto& ring_params : sRingParams) @@ -565,16 +575,17 @@ void FSManipRotateJoint::renderManipulatorRings(const LLVector3& agent_space_cen } 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); + 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); + gGL.multMatrix((GLfloat*)inv_rot_mat.mMatrix); renderCenterCircle( mRadiusMeters*1.2f, roll_color, roll_color ); } gGL.popMatrix(); @@ -619,7 +630,7 @@ void FSManipRotateJoint::renderCenterCircle(const F32 radius, const LLColor4& no LLVector3 rotationAxis = defaultNormal % targetNormal; // Cross product. F32 dot = defaultNormal * targetNormal; // Dot product. - F32 angle = acosf(dot) * RAD_TO_DEG; // Convert to degrees. + F32 angle = acosf(dot) * RAD_TO_DEG; // Convert to degrees. if (rotationAxis.magVec() > 0.001f) { @@ -664,16 +675,15 @@ void FSManipRotateJoint::renderCenterSphere(const F32 radius, const LLColor4& no // 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.flush(); } gGL.popMatrix(); } - /** * @brief Renders the joint rotation manipulator and associated visual elements. * @@ -697,72 +707,65 @@ void FSManipRotateJoint::renderCenterSphere(const F32 radius, const LLColor4& no */ void FSManipRotateJoint::render() { - // Early-out if no joint or avatar. - // Needs more something: if they log out while dots on them, asplode - if (!mJoint || !mAvatar || mAvatar->isDead()) - { + if (!isAvatarJointSafeToUse()) return; - } - + // update visibility and rotation center. bool activeJointVisible = updateVisiblity(); + // Setup GL state. LLGLSUIDefault gls_ui; gGL.getTexUnit(0)->bind(LLViewerFetchedTexture::sWhiteImagep); LLGLDepthTest gls_depth(GL_TRUE); LLGLEnable gl_blend(GL_BLEND); - + // Iterate through the avatar's joint map. // If a joint other than the currently selected is highlighted, render a pulsing sphere. // otherwise a small static sphere + LLCachedControl show_joint_markers(gSavedSettings, "FSManipShowJointMarkers", true); + LLVector3 jointLocation; 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(); - if( joint == mHighlightedJoint && joint != mJoint ) + if (joint == mJoint) + continue; + + jointLocation = joint->getWorldPosition(); + if (joint == mHighlightedJoint) { - renderPulsingSphere(joint->getWorldPosition()); - } - else if( joint != mJoint ) - { - // Render a static sphere for the joint being manipulated. - LLCachedControl show_joint_markers(gSavedSettings, "FSManipShowJointMarkers", true); - if(show_joint_markers) - { - renderStaticSphere(joint->getWorldPosition(), LLColor4(1.f, 0.5f, 0.f, 0.5f), 0.01f); - } + renderPulsingSphere(jointLocation); + continue; } + + if (show_joint_markers) + renderStaticSphere(jointLocation, LLColor4(1.f, 0.5f, 0.f, 0.5f), 0.01f); } if (!activeJointVisible) - { return; - } - - // Update joint world matrices. - mJoint->updateWorldMatrixParent(); - mJoint->updateWorldMatrix(); - - const LLQuaternion joint_world_rotation = mJoint->getWorldRotation(); + const LLQuaternion joint_world_rotation = getSelectedJointWorldRotation(); const LLQuaternion parentWorldRot = (mJoint->getParent()) ? mJoint->getParent()->getWorldRotation() : LLQuaternion::DEFAULT; LLQuaternion currentLocalRot = mJoint->getRotation(); - LLQuaternion rotatedNaturalAlignment = mNaturalAlignmentQuat * currentLocalRot; + // Compute the final world alignment: LLQuaternion final_world_alignment = rotatedNaturalAlignment * parentWorldRot; - const LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + const LLVector3 agent_space_center = mAvatar->getPosAgentFromGlobal(mRotationCenter); - LLCachedControl use_natural_direction(gSavedSettings, "FSManipRotateJointUseNaturalDirection", true); - LLQuaternion active_rotation = use_natural_direction? final_world_alignment : joint_world_rotation; + LLCachedControl use_natural_direction(gSavedSettings, "FSManipRotateJointUseNaturalDirection", true); + LLQuaternion active_rotation = use_natural_direction ? final_world_alignment : joint_world_rotation; active_rotation.normalize(); + // Render the manipulator rings in a separate function. gGL.matrixMode(LLRender::MM_MODELVIEW); renderAxes(agent_space_center, mRadiusMeters * 1.5f, active_rotation); @@ -779,13 +782,13 @@ void FSManipRotateJoint::renderAxes(const LLVector3& agent_space_center, F32 siz 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); @@ -875,9 +878,7 @@ void FSManipRotateJoint::renderNameXYZ(const LLQuaternion& rot) S32 vertical_offset = window_center_y - VERTICAL_OFFSET; LLVector3 euler_angles; - rot.getEulerAngles(&euler_angles.mV[0], - &euler_angles.mV[1], - &euler_angles.mV[2]); + rot.getEulerAngles(&euler_angles.mV[0], &euler_angles.mV[1], &euler_angles.mV[2]); euler_angles *= RAD_TO_DEG; for (S32 i = 0; i < 3; ++i) { @@ -925,15 +926,6 @@ void FSManipRotateJoint::renderNameXYZ(const LLQuaternion& rot) 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%c", getManipPartString(mManipPart).c_str(), mCamEdgeOn?'*':' '), window_center_x + 30.f, base_y, LLColor4(1.f, 1.f, .1f, 1.f)); - if (mManipPart != LL_NO_PART) - { - LL_INFOS("FSManipRotateJoint") << "Joint: " << mJoint->getName() - << ", Manip: " << getManipPartString(mManipPart) - << ", Quaternion: " << rot - << ", Euler Angles: " << mLastEuler - << ", Delta Angle: " << mLastAngle * RAD_TO_DEG - << LL_ENDL; - } } gGL.popMatrix(); @@ -954,48 +946,46 @@ void FSManipRotateJoint::renderActiveRing( F32 radius, F32 width, const LLColor4 } } - // ------------------------------------- // 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) - { + if (!isAvatarJointSafeToUse()) return false; - } // Highlight the manipulator as before. highlightManipulators(x, y); - if (mHighlightedPart != LL_NO_PART) + if (mHighlightedPart == LL_NO_PART) + return false; + + mManipPart = (EManipPart)mHighlightedPart; + + // Get the joint's center in agent space. + LLVector3 agent_space_center = mAvatar->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()) { - 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; - } + // 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 = getSelectedJointWorldRotation(); + + // Capture the mouse for dragging. + setMouseCapture(true); + return true; + } + return false; } @@ -1017,24 +1007,27 @@ 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")); + // For joint manipulation, require both a valid joint and avatar. + if (!isAvatarJointSafeToUse()) + return false; + + auto* poser = dynamic_cast(LLFloaterReg::findInstance("fs_poser")); + if (!poser) + return false; + // 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 || mAvatar->isDead() || !poser) - { - return false; - } + poser->setFocus(true); S32 hit_part = mHighlightedPart; // Save the joint’s current world rotation as the basis for the drag. - mSavedJointRot = mJoint->getWorldRotation(); - + mSavedJointRot = getSelectedJointWorldRotation(); + mLastSetRotation.set(LLQuaternion()); mManipPart = (EManipPart)hit_part; // Convert rotation center from global to agent space. - LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + LLVector3 agent_space_center = mAvatar->getPosAgentFromGlobal(mRotationCenter); // based on mManipPArt (set in highlightmanipulators). decide whether we are constrained or not in the rotation if (mManipPart == LL_ROT_GENERAL) @@ -1098,35 +1091,27 @@ bool FSManipRotateJoint::handleMouseDownOnPart(S32 x, S32 y, MASK mask) // 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 && mJoint) - { - poser->updatePosedBones(mJoint->getName()); - } - - // Release mouse + { setMouseCapture(false); mManipPart = LL_NO_PART; - mLastAngle = 0.0f; + mLastAngle = 0.0f; mCamEdgeOn = false; + return true; } - else if(mHighlightedJoint) + else if (mHighlightedJoint) { + auto* poser = dynamic_cast(LLFloaterReg::findInstance("fs_poser")); 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 * @@ -1143,7 +1128,7 @@ void FSManipRotateJoint::highlightManipulators(S32 x, S32 y) mHighlightedPart = LL_NO_PART; // Instead of using mObjectSelection->getFirstMoveableObject(), // simply require that the joint (and the avatar) is valid. - if (!mJoint || !mAvatar || mAvatar->isDead()) + if (!isAvatarJointSafeToUse()) { highlightHoverSpheres(x, y); gViewerWindow->setCursor(UI_CURSOR_ARROW); @@ -1153,25 +1138,24 @@ void FSManipRotateJoint::highlightManipulators(S32 x, S32 y) // 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); + LLVector3 agent_space_rotation_center = mAvatar->getPosAgentFromGlobal(mRotationCenter); // Update joint world matrices. mJoint->updateWorldMatrixParent(); mJoint->updateWorldMatrix(); - const LLQuaternion joint_world_rotation = mJoint->getWorldRotation(); + const LLQuaternion joint_world_rotation = getSelectedJointWorldRotation(); 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. @@ -1308,14 +1292,18 @@ bool FSManipRotateJoint::handleHover(S32 x, S32 y, MASK mask) { highlightManipulators(x, y); } + return true; } LLQuaternion FSManipRotateJoint::dragUnconstrained(S32 x, S32 y) { + if (!isAvatarJointSafeToUse()) + return LLQuaternion(); + // Get the camera position and the joint’s pivot (in agent space) LLVector3 cam = gAgentCamera.getCameraPositionAgent(); - LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + LLVector3 agent_space_center = mAvatar->getPosAgentFromGlobal(mRotationCenter); // Compute the current intersection on the sphere. mMouseCur = intersectMouseWithSphere(x, y, agent_space_center, mRadiusMeters); @@ -1388,27 +1376,29 @@ static LLQuaternion extractTwist(const LLQuaternion& rot, const LLVector3& axis) LLVector3 proj = axis * dot; // proj is now purely along 'axis' // Build the “twist” quaternion from (proj, w), then renormalize - LLQuaternion twist(proj.mV[VX], - proj.mV[VY], - proj.mV[VZ], - w); + LLQuaternion twist(proj.mV[VX], proj.mV[VY], proj.mV[VZ], w); if (w < 0.f) - { + { twist = -twist; } twist.normalize(); return twist; } + LLQuaternion FSManipRotateJoint::dragConstrained(S32 x, S32 y) { + if (!isAvatarJointSafeToUse()) + return LLQuaternion(); + // Get the constraint axis from our joint manipulator. LLVector3 constraint_axis = getConstraintAxis(); - LLVector3 agent_space_center = gAgent.getPosAgentFromGlobal(mRotationCenter); + LLVector3 agent_space_center = mAvatar->getPosAgentFromGlobal(mRotationCenter); if (mCamEdgeOn) { LLQuaternion freeRot = dragUnconstrained(x, y); return extractTwist(freeRot, constraint_axis); } + // 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); @@ -1416,6 +1406,7 @@ LLQuaternion FSManipRotateJoint::dragConstrained(S32 x, S32 y) { return LLQuaternion::DEFAULT; } + projected_mouse -= agent_space_center; projected_mouse.normalize(); @@ -1424,6 +1415,7 @@ LLQuaternion FSManipRotateJoint::dragConstrained(S32 x, S32 y) initial_proj -= (initial_proj * constraint_axis) * constraint_axis; initial_proj.normalize(); + //float angle = acos(initial_proj * projected_mouse); // angle in (-pi, pi) // 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; @@ -1431,14 +1423,16 @@ LLQuaternion FSManipRotateJoint::dragConstrained(S32 x, S32 y) 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; + if (!updateVisiblity()) + return; - LLQuaternion delta_rot; + LLQuaternion delta_send, delta_rot; if (mManipPart == LL_ROT_GENERAL) { delta_rot = dragUnconstrained(x, y); @@ -1447,10 +1441,31 @@ void FSManipRotateJoint::drag(S32 x, S32 y) { 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); + + delta_send.set(delta_rot); + delta_send *= ~mLastSetRotation; + mLastSetRotation.set(delta_rot); + delta_send = mSavedJointRot * delta_send * ~mSavedJointRot; + + switch (mReferenceFrame) + { + case POSER_FRAME_CAMERA: + case POSER_FRAME_AVATAR: + delta_send.conjugate(); + break; + + case POSER_FRAME_WORLD: + delta_send.mQ[VX] *= -1; + break; + + case POSER_FRAME_BONE: + default: + break; + } + + auto* poser = dynamic_cast(LLFloaterReg::findInstance("fs_poser")); + if (poser && mJoint) + poser->updatePosedBones(mJoint->getName(), delta_send, LLVector3::zero, LLVector3::zero); } // set mConstrainedAxis based on mManipParat and returns it too. @@ -1478,10 +1493,10 @@ LLVector3 FSManipRotateJoint::setConstraintAxis() // Transform the local axis into world space using the joint's world rotation. if (mJoint) { - LLCachedControl use_natural_direction(gSavedSettings, "FSManipRotateJointUseNaturalDirection", true); + 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; @@ -1493,12 +1508,41 @@ LLVector3 FSManipRotateJoint::setConstraintAxis() } else { - active_rotation = mJoint->getWorldRotation(); + active_rotation = getSelectedJointWorldRotation(); } axis = axis * active_rotation; axis.normalize(); } } + mConstraintAxis = axis; + return axis; } + +LLQuaternion FSManipRotateJoint::getSelectedJointWorldRotation() +{ + LLQuaternion joinRot; + if (!mJoint || !mAvatar) + return joinRot; + + auto* poser = dynamic_cast(LLFloaterReg::findInstance("fs_poser")); + if (!poser) + return joinRot; + + return poser->getManipGimbalRotation(mJoint->getName()); +} + +bool FSManipRotateJoint::isAvatarJointSafeToUse() +{ + if (!mJoint || !mAvatar) + return false; + + if (mAvatar->isDead() || !mAvatar->isFullyLoaded()) + { + setAvatar(nullptr); + return false; + } + + return true; +} diff --git a/indra/newview/fsmaniprotatejoint.h b/indra/newview/fsmaniprotatejoint.h index aaad19bfe6..522bfe390c 100644 --- a/indra/newview/fsmaniprotatejoint.h +++ b/indra/newview/fsmaniprotatejoint.h @@ -1,5 +1,5 @@ /** - * @file fsmaniproatejoint.h + * @file fsmaniprotatejoint.h * @brief custom manipulator for rotating joints * * $LicenseInfo:firstyear=2024&license=viewerlgpl$ @@ -32,11 +32,22 @@ #include "llmaniprotate.h" class LLJoint; -class LLVOAvatar; // or LLVOAvatarSelf, etc. +class LLVOAvatar; // for LLVOAvatarSelf, etc. + +/// +/// A set of reference frames for presenting the gimbal within. +/// +typedef enum E_PoserReferenceFrame +{ + POSER_FRAME_BONE = 0, // frame is the bone the gimbal is centered on + POSER_FRAME_WORLD = 1, // frame is world North-South-East-West-Up-Down + POSER_FRAME_AVATAR = 2, // frame is mPelvis rotation + POSER_FRAME_CAMERA = 3, // frame is defined by vector of camera position to bone position +} E_PoserReferenceFrame; namespace { const F32 AXIS_ONTO_CAM_TOLERANCE = cos( 85.f * DEG_TO_RAD ); // cos() is not constexpr til c++26 - constexpr F32 RADIUS_PIXELS = 100.f; // size in screen space + 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; @@ -74,11 +85,25 @@ public: FSManipRotateJoint(LLToolComposite* composite); virtual ~FSManipRotateJoint() {} static std::string getManipPartString(EManipPart part); - // Called to designate which joint we are going to manipulate. + + /// + /// Sets the joint we are going to manipulate. + /// + /// The joint to interact with. void setJoint(LLJoint* joint); + /// + /// Sets the avatar the manip should interact with. + /// + /// The avatar to interact with. void setAvatar(LLVOAvatar* avatar); + /// + /// Sets the reference frame for the manipulator. + /// + /// The E_PoserReferenceFrame to use. + void setReferenceFrame(const E_PoserReferenceFrame frame) { mReferenceFrame = frame; }; + // Overrides void handleSelect() override; bool updateVisiblity() override; @@ -138,8 +163,13 @@ private: void renderRingPass(const RingRenderParams& params, float radius, float width, int pass); void renderAxes(const LLVector3& center, F32 size, const LLQuaternion& rotation); + bool isAvatarJointSafeToUse(); + LLQuaternion getSelectedJointWorldRotation(); + float mLastAngle = 0.f; LLVector3 mConstraintAxis; + E_PoserReferenceFrame mReferenceFrame = POSER_FRAME_BONE; + LLQuaternion mLastSetRotation; }; #endif // FS_MANIP_ROTATE_JOINT_H diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index 067c4dbdd0..fe7a1b67dc 100644 --- a/indra/newview/fsposeranimator.cpp +++ b/indra/newview/fsposeranimator.cpp @@ -29,6 +29,7 @@ #include "fsposeranimator.h" #include "llcharacter.h" #include "llagent.h" +#include "llagentcamera.h" #include "fsposingmotion.h" std::map FSPoserAnimator::sAvatarIdToRegisteredAnimationId; @@ -52,6 +53,25 @@ bool FSPoserAnimator::isPosingAvatarJoint(LLVOAvatar* avatar, const FSPoserJoint return posingMotion->currentlyPosingJoint(jointPose); } +bool FSPoserAnimator::hasJointBeenChanged(LLVOAvatar* avatar, const FSPoserJoint& joint) +{ + if (!isAvatarSafeToUse(avatar)) + return false; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return false; + + if (posingMotion->isStopped()) + return false; + + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); + if (!jointPose) + return false; + + return jointPose->getJointModified(); +} + void FSPoserAnimator::setPosingAvatarJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, bool shouldPose) { if (!isAvatarSafeToUse(avatar)) @@ -212,7 +232,8 @@ LLVector3 FSPoserAnimator::getJointPosition(LLVOAvatar* avatar, const FSPoserJoi return jointPose->getPublicPosition(); } -void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& position, E_BoneDeflectionStyles style) +void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& position, E_PoserReferenceFrame frame, + E_BoneDeflectionStyles style) { if (!isAvatarSafeToUse(avatar)) return; @@ -231,7 +252,8 @@ void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* j if (!jointPose) return; - LLVector3 positionDelta = jointPose->getPublicPosition() - position; + LLVector3 jointPosition = jointPose->getPublicPosition(); + LLVector3 positionDelta = jointPosition - position; switch (style) { @@ -239,13 +261,13 @@ void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* j case MIRROR_DELTA: case SYMPATHETIC_DELTA: case SYMPATHETIC: - jointPose->setPublicPosition(position); + jointPose->setPublicPosition(jointPosition - positionDelta); break; case DELTAMODE: case NONE: default: - jointPose->setPublicPosition(position); + jointPose->setPublicPosition(jointPosition - positionDelta); return; } @@ -406,6 +428,7 @@ void FSPoserAnimator::setAllAvatarStartingRotationsToZero(LLVOAvatar* avatar) return; posingMotion->setAllRotationsToZeroAndClearUndo(); + mPosingState.purgeMotionStates(avatar); for (size_t index = 0; index != PoserJoints.size(); ++index) { @@ -420,10 +443,9 @@ void FSPoserAnimator::setAllAvatarStartingRotationsToZero(LLVOAvatar* avatar) posingMotion->setJointBvhLock(jointPose, false); } - } -void FSPoserAnimator::recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneAxisTranslation translation, S32 negation) +void FSPoserAnimator::recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joint) { if (!isAvatarSafeToUse(avatar)) return; @@ -440,8 +462,9 @@ void FSPoserAnimator::recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joi setPosingAvatarJoint(avatar, joint, true); } -void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoint* joint, bool resetBaseRotationToZero, - E_BoneDeflectionStyles style) +void FSPoserAnimator::updateJointFromManip(LLVOAvatar* avatar, const FSPoserJoint* joint, bool resetBaseRotationToZero, + E_BoneDeflectionStyles style, E_PoserReferenceFrame frame, const LLQuaternion rotation, + const LLVector3 position, const LLVector3 scale) { if (!isAvatarSafeToUse(avatar)) return; @@ -454,27 +477,34 @@ void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoi if (!jointPose) return; - LLQuaternion deltaRot = jointPose->recaptureJointAsDelta(resetBaseRotationToZero); + LLQuaternion framedRotation = changeToRotationFrame(avatar, rotation, frame, jointPose); + jointPose->setPublicRotation(resetBaseRotationToZero, framedRotation * jointPose->getPublicRotation()); - deRotateWorldLockedDescendants(joint, posingMotion, deltaRot); + deRotateWorldLockedDescendants(joint, posingMotion, framedRotation); if (style == NONE || style == DELTAMODE) return; + auto oppositePoserJoint = getPoserJointByName(joint->mirrorJointName()); FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint->mirrorJointName()); if (!oppositeJointPose) return; + LLQuaternion mirroredRotation = LLQuaternion(-framedRotation.mQ[VX], framedRotation.mQ[VY], -framedRotation.mQ[VZ], framedRotation.mQ[VW]); switch (style) { case SYMPATHETIC: case SYMPATHETIC_DELTA: - oppositeJointPose->cloneRotationFrom(jointPose); + oppositeJointPose->setPublicRotation(resetBaseRotationToZero, framedRotation * oppositeJointPose->getPublicRotation()); + if (oppositePoserJoint) + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, framedRotation); break; case MIRROR: case MIRROR_DELTA: - oppositeJointPose->mirrorRotationFrom(jointPose); + oppositeJointPose->setPublicRotation(resetBaseRotationToZero, mirroredRotation * oppositeJointPose->getPublicRotation()); + if (oppositePoserJoint) + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, mirroredRotation); break; default: @@ -482,6 +512,54 @@ void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoi } } +LLQuaternion FSPoserAnimator::getManipGimbalRotation(LLVOAvatar* avatar, const FSPoserJoint* joint, E_PoserReferenceFrame frame) +{ + LLQuaternion globalRot(-1.f, 0.f, 0.f, 0.f); + if (frame == POSER_FRAME_WORLD) + return globalRot; + + if (!joint) + return globalRot; + + if (!isAvatarSafeToUse(avatar)) + return globalRot; + + if (frame == POSER_FRAME_AVATAR) + { + LLJoint* pelvis = avatar->getJoint("mPelvis"); + if (pelvis) + return pelvis->getWorldRotation(); + + return globalRot; + } + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return globalRot; + + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint->jointName()); + if (!jointPose) + return globalRot; + + LLJoint* llJoint = jointPose->getJointState()->getJoint(); + if (!llJoint) + return globalRot; + + if (frame == POSER_FRAME_BONE) + return llJoint->getWorldRotation(); + + LLVector3 skyward(0.f, 0.f, 1.f); + LLVector3 left(1.f, 0.f, 0.f); + LLVector3 up, jointToCameraPosition, jointPosition; + jointPosition = llJoint->getWorldPosition(); + jointToCameraPosition = jointPosition - gAgentCamera.getCameraPositionAgent(); + jointToCameraPosition.normalize(); + left.setVec(skyward % jointToCameraPosition); + up.setVec(jointToCameraPosition % left); + + return LLQuaternion(jointToCameraPosition, left, up); +} + LLVector3 FSPoserAnimator::getJointExportRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, bool lockWholeAvatar) const { auto rotation = getJointRotation(avatar, joint, SWAP_NOTHING, NEGATE_NOTHING); @@ -525,12 +603,13 @@ LLVector3 FSPoserAnimator::getJointRotation(LLVOAvatar* avatar, const FSPoserJoi if (!jointPose) return vec3; - return translateRotationFromQuaternion(translation, negation, jointPose->getPublicRotation()); + return translateRotationFromQuaternion(jointPose, translation, negation, jointPose->getPublicRotation()); } void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& absRotation, - const LLVector3& deltaRotation, E_BoneDeflectionStyles deflectionStyle, - E_BoneAxisTranslation translation, S32 negation, bool resetBaseRotationToZero, E_RotationStyle rotationStyle) + const LLVector3& deltaRotation, E_BoneDeflectionStyles style, E_PoserReferenceFrame frame, + E_BoneAxisTranslation translation, S32 negation, bool resetBaseRotationToZero, + E_RotationStyle rotationStyle) { if (!isAvatarSafeToUse(avatar)) return; @@ -545,13 +624,15 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j if (!jointPose) return; - LLQuaternion absRot = translateRotationToQuaternion(translation, negation, absRotation); - LLQuaternion deltaRot = translateRotationToQuaternion(translation, negation, deltaRotation); - switch (deflectionStyle) + bool translationRequiresDelta = frame != POSER_FRAME_BONE; + + LLQuaternion absRot = translateRotationToQuaternion(avatar, jointPose, frame, translation, negation, absRotation); + LLQuaternion deltaRot = translateRotationToQuaternion(avatar, jointPose, frame, translation, negation, deltaRotation); + switch (style) { case SYMPATHETIC: case MIRROR: - if (rotationStyle == DELTAIC_ROT) + if (rotationStyle == DELTAIC_ROT || translationRequiresDelta) jointPose->setPublicRotation(resetBaseRotationToZero, deltaRot * jointPose->getPublicRotation()); else jointPose->setPublicRotation(resetBaseRotationToZero, absRot); @@ -570,7 +651,7 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j case NONE: default: - if (rotationStyle == DELTAIC_ROT) + if (rotationStyle == DELTAIC_ROT || translationRequiresDelta) jointPose->setPublicRotation(resetBaseRotationToZero, deltaRot * jointPose->getPublicRotation()); else jointPose->setPublicRotation(resetBaseRotationToZero, absRot); @@ -586,8 +667,8 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j if (!oppositeJointPose) return; - LLQuaternion inv_quat = LLQuaternion(-deltaRot.mQ[VX], deltaRot.mQ[VY], -deltaRot.mQ[VZ], deltaRot.mQ[VW]); - switch (deflectionStyle) + LLQuaternion mirroredRotation = LLQuaternion(-deltaRot.mQ[VX], deltaRot.mQ[VY], -deltaRot.mQ[VZ], deltaRot.mQ[VW]); + switch (style) { case SYMPATHETIC: oppositeJointPose->cloneRotationFrom(jointPose); @@ -604,13 +685,13 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j case MIRROR: oppositeJointPose->mirrorRotationFrom(jointPose); if (oppositePoserJoint) - deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, inv_quat); + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, mirroredRotation); break; case MIRROR_DELTA: - oppositeJointPose->setPublicRotation(resetBaseRotationToZero, inv_quat * oppositeJointPose->getPublicRotation()); + oppositeJointPose->setPublicRotation(resetBaseRotationToZero, mirroredRotation * oppositeJointPose->getPublicRotation()); if (oppositePoserJoint) - deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, inv_quat); + deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, mirroredRotation); break; default: @@ -709,8 +790,9 @@ void FSPoserAnimator::flipEntirePose(LLVOAvatar* avatar) } } -// from the UI to the bone, the inverse translation, the un-swap, the backwards -LLQuaternion FSPoserAnimator::translateRotationToQuaternion(E_BoneAxisTranslation translation, S32 negation, LLVector3 rotation) +// from the UI to the bone. Bone rotations we store are relative to the skeleton (or to T-Pose, if you want to visualize). +LLQuaternion FSPoserAnimator::translateRotationToQuaternion(LLVOAvatar* avatar, FSJointPose* joint, E_PoserReferenceFrame frame, + E_BoneAxisTranslation translation, S32 negation, LLVector3 rotation) { if (negation & NEGATE_ALL) { @@ -728,7 +810,7 @@ LLQuaternion FSPoserAnimator::translateRotationToQuaternion(E_BoneAxisTranslatio rotation.mV[VZ] *= -1; } - LLMatrix3 rot_mat; + LLMatrix3 rot_mat; switch (translation) { case SWAP_YAW_AND_ROLL: @@ -761,11 +843,63 @@ LLQuaternion FSPoserAnimator::translateRotationToQuaternion(E_BoneAxisTranslatio rot_quat = LLQuaternion(rot_mat) * rot_quat; rot_quat.normalize(); + rot_quat = changeToRotationFrame(avatar, rot_quat, frame, joint); + return rot_quat; } +LLQuaternion FSPoserAnimator::changeToRotationFrame(LLVOAvatar* avatar, LLQuaternion rotation, E_PoserReferenceFrame frame, FSJointPose* joint) +{ + if (!joint || !avatar) + return rotation; + + LLJoint* pelvis = avatar->getJoint("mPelvis"); + if (!pelvis) + return rotation; + + LLVector3 skyward(0.f, 0.f, 1.f); + LLVector3 left(1.f, 0.f, 0.f); + LLVector3 forwards(0.f, 1.f, 0.f); + LLVector3 up, jointToCameraPosition, jointPosition; + LLQuaternion worldRotOfWorld(forwards, left, skyward); + LLQuaternion differenceInWorldRot, rotDiffInChildFrame, worldRotOfPelvis, worldRotOfCamera; + LLQuaternion worldRotOfThisJoint = joint->getJointState()->getJoint()->getWorldRotation(); + + switch (frame) + { + case POSER_FRAME_WORLD: + differenceInWorldRot = worldRotOfThisJoint * ~worldRotOfWorld; + break; + + case POSER_FRAME_AVATAR: + worldRotOfPelvis = pelvis->getWorldRotation(); + differenceInWorldRot = worldRotOfThisJoint * ~worldRotOfPelvis; + break; + + case POSER_FRAME_CAMERA: + jointPosition = joint->getJointState()->getJoint()->getWorldPosition(); + jointToCameraPosition = jointPosition - gAgentCamera.getCameraPositionAgent(); + jointToCameraPosition.normalize(); + left.setVec(skyward % jointToCameraPosition); + up.setVec(jointToCameraPosition % left); + + worldRotOfCamera = LLQuaternion(jointToCameraPosition, left, up); + differenceInWorldRot = worldRotOfThisJoint * ~worldRotOfCamera; + break; + + case POSER_FRAME_BONE: + default: + return rotation; + } + + rotDiffInChildFrame = differenceInWorldRot * rotation * ~differenceInWorldRot; + rotDiffInChildFrame.conjugate(); + + return rotDiffInChildFrame; +} + // from the bone to the UI; this is the 'forwards' use of the enum -LLVector3 FSPoserAnimator::translateRotationFromQuaternion(E_BoneAxisTranslation translation, S32 negation, const LLQuaternion& rotation) const +LLVector3 FSPoserAnimator::translateRotationFromQuaternion(FSJointPose* joint, E_BoneAxisTranslation translation, S32 negation, const LLQuaternion& rotation) const { LLVector3 vec3; @@ -833,7 +967,8 @@ LLVector3 FSPoserAnimator::getJointScale(LLVOAvatar* avatar, const FSPoserJoint& return jointPose->getPublicScale(); } -void FSPoserAnimator::setJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& scale, E_BoneDeflectionStyles style) +void FSPoserAnimator::setJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& scale, E_PoserReferenceFrame frame, + E_BoneDeflectionStyles style) { if (!isAvatarSafeToUse(avatar)) return; @@ -852,25 +987,46 @@ void FSPoserAnimator::setJointScale(LLVOAvatar* avatar, const FSPoserJoint* join if (!jointPose) return; - jointPose->setPublicScale(scale); - FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint->mirrorJointName()); - if (!oppositeJointPose) - return; + LLVector3 jointScale = jointPose->getPublicScale(); + LLVector3 scaleDelta = jointScale - scale; switch (style) { - case SYMPATHETIC: case MIRROR: - case SYMPATHETIC_DELTA: case MIRROR_DELTA: - oppositeJointPose->setPublicScale(scale); + case SYMPATHETIC_DELTA: + case SYMPATHETIC: + jointPose->setPublicScale(jointScale - scaleDelta); break; case DELTAMODE: case NONE: default: + jointPose->setPublicScale(jointScale - scaleDelta); return; } + + FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint->mirrorJointName()); + if (!oppositeJointPose) + return; + + LLVector3 oppositeJointScale = oppositeJointPose->getPublicScale(); + + switch (style) + { + case MIRROR: + case MIRROR_DELTA: + oppositeJointPose->setPublicScale(oppositeJointScale + scaleDelta); + break; + + case SYMPATHETIC_DELTA: + case SYMPATHETIC: + oppositeJointPose->setPublicScale(oppositeJointScale - scaleDelta); + break; + + default: + break; + } } bool FSPoserAnimator::tryGetJointSaveVectors(LLVOAvatar* avatar, const FSPoserJoint& joint, LLVector3* rot, LLVector3* pos, @@ -914,7 +1070,7 @@ void FSPoserAnimator::loadJointRotation(LLVOAvatar* avatar, const FSPoserJoint* jointPose->purgeUndoQueue(); - LLQuaternion rot = translateRotationToQuaternion(SWAP_NOTHING, NEGATE_NOTHING, rotation); + LLQuaternion rot = translateRotationToQuaternion(avatar, jointPose, POSER_FRAME_BONE, SWAP_NOTHING, NEGATE_NOTHING, rotation); jointPose->setPublicRotation(setBaseToZero, rot); } @@ -963,13 +1119,21 @@ void FSPoserAnimator::loadJointScale(LLVOAvatar* avatar, const FSPoserJoint* joi } } -bool FSPoserAnimator::loadPosingState(LLVOAvatar* avatar, LLSD pose) +bool FSPoserAnimator::loadPosingState(LLVOAvatar* avatar, bool ignoreOwnership, LLSD pose) { if (!isAvatarSafeToUse(avatar)) return false; mPosingState.purgeMotionStates(avatar); - mPosingState.restoreMotionStates(avatar, pose); + mPosingState.restoreMotionStates(avatar, ignoreOwnership, pose); + + return applyStatesToPosingMotion(avatar); +} + +bool FSPoserAnimator::applyStatesToPosingMotion(LLVOAvatar* avatar) +{ + if (!isAvatarSafeToUse(avatar)) + return false; FSPosingMotion* posingMotion = getPosingMotion(avatar); if (!posingMotion) @@ -1007,9 +1171,9 @@ void FSPoserAnimator::applyJointMirrorToBaseRotations(FSPosingMotion* posingMoti } } -void FSPoserAnimator::savePosingState(LLVOAvatar* avatar, LLSD* saveRecord) +void FSPoserAnimator::savePosingState(LLVOAvatar* avatar, bool ignoreOwnership, LLSD* saveRecord) { - mPosingState.writeMotionStates(avatar, saveRecord); + mPosingState.writeMotionStates(avatar, ignoreOwnership, saveRecord); } const FSPoserAnimator::FSPoserJoint* FSPoserAnimator::getPoserJointByName(const std::string& jointName) const @@ -1023,6 +1187,39 @@ const FSPoserAnimator::FSPoserJoint* FSPoserAnimator::getPoserJointByName(const return nullptr; } +const FSPoserAnimator::FSPoserJoint* FSPoserAnimator::getPoserJointByNumber(LLVOAvatar* avatar, const int jointNumber) const +{ + if (!avatar) + return nullptr; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return nullptr; + + FSJointPose* parentJoint = posingMotion->getJointPoseByJointNumber(jointNumber); + if (!parentJoint) + return nullptr; + + return getPoserJointByName(parentJoint->jointName()); +} + +bool FSPoserAnimator::tryGetJointNumber(LLVOAvatar* avatar, const FSPoserJoint &poserJoint, int &jointNumber) +{ + if (!avatar) + return false; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return false; + + FSJointPose* parentJoint = posingMotion->getJointPoseByJointName(poserJoint.jointName()); + if (!parentJoint) + return false; + + jointNumber = parentJoint->getJointNumber(); + return jointNumber >= 0; +} + bool FSPoserAnimator::tryPosingAvatar(LLVOAvatar* avatar) { if (!isAvatarSafeToUse(avatar)) @@ -1038,7 +1235,6 @@ bool FSPoserAnimator::tryPosingAvatar(LLVOAvatar* avatar) gAgent.stopFidget(); mPosingState.captureMotionStates(avatar); - avatar->startDefaultMotions(); avatar->startMotion(posingMotion->motionId()); @@ -1057,11 +1253,17 @@ void FSPoserAnimator::updatePosingState(LLVOAvatar* avatar, std::vector jointNumbersRecaptured; for (auto item : jointsRecaptured) - jointNamesRecaptured += item->jointName(); + { + auto poserJoint = posingMotion->getJointPoseByJointName(item->jointName()); + if (!poserJoint) + continue; - mPosingState.updateMotionStates(avatar, posingMotion, jointNamesRecaptured); + jointNumbersRecaptured.push_back(poserJoint->getJointNumber()); + } + + mPosingState.updateMotionStates(avatar, posingMotion, jointNumbersRecaptured); } void FSPoserAnimator::stopPosingAvatar(LLVOAvatar *avatar) @@ -1219,6 +1421,9 @@ void FSPoserAnimator::undoOrRedoWorldLockedDescendants(const FSPoserJoint& joint void FSPoserAnimator::undoOrRedoJointOrFirstLockedChild(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo) { + if (!posingMotion) + return; + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); if (!jointPose) return; diff --git a/indra/newview/fsposeranimator.h b/indra/newview/fsposeranimator.h index 880997a750..3d3bc02163 100644 --- a/indra/newview/fsposeranimator.h +++ b/indra/newview/fsposeranimator.h @@ -30,6 +30,7 @@ #include "fsposingmotion.h" #include "fsposestate.h" #include "llvoavatar.h" +#include "fsmaniprotatejoint.h" /// /// Describes how we will cluster the joints/bones/thingos. @@ -395,6 +396,20 @@ public: /// The matching joint if found, otherwise nullptr const FSPoserJoint* getPoserJointByName(const std::string& jointName) const; + /// + /// Get a PoserJoint case-insensitive-matching the supplied name. + /// + /// The name of the joint to match. + /// The matching joint if found, otherwise nullptr + const FSPoserJoint* getPoserJointByNumber(LLVOAvatar* avatar, const int jointNumber) const; + + /// + /// Get a PoserJoint by its LLJoint number. + /// + /// The name of the joint to match. + /// The matching joint if found, otherwise nullptr + bool tryGetJointNumber(LLVOAvatar* avatar, const FSPoserJoint &poserJoint, int &jointNumber); + /// /// Tries to start posing the supplied avatar. /// @@ -423,6 +438,14 @@ public: /// True if this is joint is being posed for the supplied avatar, otherwise false. bool isPosingAvatarJoint(LLVOAvatar* avatar, const FSPoserJoint& joint); + /// + /// Determines whether the supplied PoserJoint for the supplied avatar has been modified this session, even if all change has been reverted. + /// + /// The avatar having the joint to which we refer. + /// The joint being queried for. + /// True if this is joint has been changed while posing even if the change has been reverted or undone, otherwise false. + bool hasJointBeenChanged(LLVOAvatar* avatar, const FSPoserJoint& joint); + /// /// Sets whether the supplied PoserJoint for the supplied avatar should be posed. /// @@ -479,8 +502,10 @@ public: /// The avatar whose joint is to be set. /// The joint to set. /// The position to set the joint to. + /// The frame to translate the position to. /// Any ancilliary action to be taken with the change to be made. - void setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& position, E_BoneDeflectionStyles style); + void setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& position, E_PoserReferenceFrame frame, + E_BoneDeflectionStyles style); /// /// Gets the rotation of a joint for the supplied avatar. @@ -506,6 +531,15 @@ public: /// LLVector3 getJointExportRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, bool lockWholeAvatar) const; + /// + /// Gets the rotation suitable for the Manip gimbal for the supplied avatar and joint. + /// + /// The avatar having the Manip gimbal placed upon it. + /// The joint on the avatar where the manip should be placed. + /// The frame of reference for the gimbal. + /// The rotation to set the gimbal to. + LLQuaternion getManipGimbalRotation(LLVOAvatar* avatar, const FSPoserJoint* joint, E_PoserReferenceFrame frame); + /// /// Sets the rotation of a joint for the supplied avatar. /// @@ -518,8 +552,9 @@ public: /// The style of negation to apply to the set. /// Whether to set the base rotation to zero on setting the rotation. /// Whether to apply the supplied rotation as a delta to the supplied joint. - void setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& absRotation, const LLVector3& deltaRotation, E_BoneDeflectionStyles style, - E_BoneAxisTranslation translation, S32 negation, bool resetBaseRotationToZero, E_RotationStyle rotationStyle); + void setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& absRotation, const LLVector3& deltaRotation, + E_BoneDeflectionStyles style, E_PoserReferenceFrame frame, E_BoneAxisTranslation translation, S32 negation, + bool resetBaseRotationToZero, E_RotationStyle rotationStyle); /// /// Gets the scale of a joint for the supplied avatar. @@ -535,8 +570,10 @@ public: /// The avatar whose joint is to be set. /// The joint to set. /// The scale to set the joint to. + /// The frame to translate the position to. /// Any ancilliary action to be taken with the change to be made. - void setJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& scale, E_BoneDeflectionStyles style); + void setJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, const LLVector3& scale, E_PoserReferenceFrame frame, + E_BoneDeflectionStyles style); /// /// Reflects the joint with its opposite if it has one, or just mirror the rotation of itself. @@ -564,9 +601,7 @@ public: /// /// 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 recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joint); /// /// Recaptures any change in joint state. @@ -575,7 +610,11 @@ public: /// The joint to recapture. /// Whether to set the base rotation to zero on setting the rotation. /// Any ancilliary action to be taken with the change to be made. - void recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoint* joint, bool resetBaseRotationToZero, E_BoneDeflectionStyles style); + /// The rotation of the supplied joint. + /// The position of the supplied joint. + /// The scale of the supplied joint. + void updateJointFromManip(LLVOAvatar* avatar, const FSPoserJoint* joint, bool resetBaseRotationToZero, E_BoneDeflectionStyles style, + E_PoserReferenceFrame frame, const LLQuaternion rotation, const LLVector3 position, const LLVector3 scale); /// /// Sets all of the joint rotations of the supplied avatar to zero. @@ -711,6 +750,7 @@ public: /// Loads the posing state (base rotations) to the supplied avatars posing-motion, from the supplied record. /// /// That avatar whose posing state should be loaded. + /// Whether to ignore ownership. For use when reading a local file. /// The record to read the posing state from. /// True if the pose loaded successfully, otherwise false. /// @@ -718,14 +758,22 @@ public: /// it can take several frames for the animation to be loaded and ready. /// It may therefore be necessary to attempt this several times. /// - bool loadPosingState(LLVOAvatar* avatar, LLSD pose); + bool loadPosingState(LLVOAvatar* avatar, bool ignoreOwnership, LLSD pose); + + /// + /// Applies the posing states to the posing motion for the supplied avatar. + /// + /// That avatar whose posing state should be loaded. + /// True if the state applied successfully, otherwise false. + bool applyStatesToPosingMotion(LLVOAvatar* avatar); /// /// Adds the posing state for the supplied avatar to the supplied record. /// /// That avatar whose posing state should be written. + /// Whether to ignore ownership while saving. /// The record to write the posing state to. - void savePosingState(LLVOAvatar* avatar, LLSD* saveRecord); + void savePosingState(LLVOAvatar* avatar, bool ignoreOwnership, LLSD* saveRecord); /// /// Purges and recaptures the pose state for the supplied avatar. @@ -734,17 +782,6 @@ public: /// The joints which were recaptured. void updatePosingState(LLVOAvatar* avatar, std::vector jointsRecaptured); - /// - /// Add a new posing state, or updates the matching posing state with the supplied data. - /// - /// The avatar the posing state is intended for. - /// The ID of the animation. - /// The frame-time of the animation. - /// The names of the joints, if any, the animation should specifically be applied to. - /// The capture order. - /// True if the posing state was added or changed by the update data, otherwise false. - bool addOrUpdatePosingState(LLVOAvatar* avatar, LLUUID animId, F32 updateTime, std::string jointNames, int captureOrder); - /// /// Traverses the joints and applies reversals to the base rotations if needed. /// @@ -755,14 +792,19 @@ public: void applyJointMirrorToBaseRotations(FSPosingMotion* posingMotion); private: - /// - /// Translates a rotation vector from the UI to a Quaternion for the bone. - /// This also performs the axis-swapping the UI needs for up/down/left/right to make sense. - /// - /// The axis translation to perform. - /// The rotation to transform to quaternion. - /// The rotation quaternion. - LLQuaternion translateRotationToQuaternion(E_BoneAxisTranslation translation, S32 negation, LLVector3 rotation); + /// + /// Translates the supplied rotation vector from UI to a Quaternion for the bone. + /// Also performs the axis-swapping and other transformations for up/down/left/right to make sense. + /// + /// The avatar whose joint is being manipulated. + /// The joint which is being altered. + /// The frame of reference the translation should be performed in. + /// The axis translation to perform. + /// The style of axis-negation. + /// The rotation to translate and transform to quaternion. + /// The translated rotation quaternion. + LLQuaternion translateRotationToQuaternion(LLVOAvatar* avatar, FSJointPose* joint, E_PoserReferenceFrame frame, + E_BoneAxisTranslation translation, S32 negation, LLVector3 rotation); /// /// Translates a bone-rotation quaternion to a vector usable easily on the UI. @@ -770,7 +812,7 @@ public: /// The axis translation to perform. /// The rotation to transform to matrix. /// The rotation vector. - LLVector3 translateRotationFromQuaternion(E_BoneAxisTranslation translation, S32 negation, const LLQuaternion& rotation) const; + LLVector3 translateRotationFromQuaternion(FSJointPose* joint, E_BoneAxisTranslation translation, S32 negation, const LLQuaternion& rotation) const; /// /// Creates a posing motion for the supplied avatar. @@ -844,6 +886,20 @@ public: /// Whether to redo the edit, otherwise the edit is undone. void undoOrRedoJointOrFirstLockedChild(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo); + /// + /// Converts the supplied rotation into the desired frame. + /// + /// The avatar owning the supplied joint. + /// The rotation to convert. + /// The frame to translate the rotation to. + /// The joint whose rotation is being changed. + /// + /// Input rotations have no implicit frame: it's just a rotation and ordinarily applied, inherits the joint's rotational framing. + /// This method imposes a framing upon the supplied rotation, meaning user input is considered as relative to something like + /// 'the world', 'avatar pelvis' or the position of the camera relative to the joint. + /// + LLQuaternion changeToRotationFrame(LLVOAvatar* avatar, LLQuaternion rotation, E_PoserReferenceFrame frame, FSJointPose* joint); + /// /// Maps the avatar's ID to the animation registered to them. /// Thus we start/stop the same animation, and get/set the same rotations etc. diff --git a/indra/newview/fsposestate.cpp b/indra/newview/fsposestate.cpp index ea94565d7d..804a327220 100644 --- a/indra/newview/fsposestate.cpp +++ b/indra/newview/fsposestate.cpp @@ -3,6 +3,7 @@ std::map> FSPoseState::sMotionStates; std::map FSPoseState::sCaptureOrder; +std::map FSPoseState::sMotionStatesOwnedByMe; void FSPoseState::captureMotionStates(LLVOAvatar* avatar) { @@ -10,6 +11,7 @@ void FSPoseState::captureMotionStates(LLVOAvatar* avatar) return; sCaptureOrder[avatar->getID()] = 0; + int animNumber = 0; for (auto anim_it = avatar->mPlayingAnimations.begin(); anim_it != avatar->mPlayingAnimations.end(); ++anim_it) { @@ -21,27 +23,26 @@ void FSPoseState::captureMotionStates(LLVOAvatar* avatar) newState.motionId = anim_it->first; newState.lastUpdateTime = motion->getLastUpdateTime(); newState.captureOrder = 0; - newState.avatarOwnsPose = canSaveMotionId(avatar, anim_it->first); + newState.inLayerOrder = animNumber++; + newState.gAgentOwnsPose = canSaveMotionId(avatar, anim_it->first); sMotionStates[avatar->getID()].push_back(newState); } } -void FSPoseState::updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingMotion, std::string jointNamesRecaptured) +void FSPoseState::updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingMotion, std::vector jointNumbersRecaptured) { if (!avatar || !posingMotion) return; sCaptureOrder[avatar->getID()]++; + int animNumber = 0; - // if an animation for avatar is a subset of jointNamesRecaptured, delete it + // if an animation for avatar is a subset of jointNumbersRecaptured, delete it // this happens on second/subsequent recaptures; the first recapture is no longer needed for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end();) { - std::string joints = (*it).jointNamesAnimated; - bool recaptureMatches = !joints.empty() && !jointNamesRecaptured.empty() && jointNamesRecaptured.find(joints) != std::string::npos; - - if (recaptureMatches) + if (vector2IsSubsetOfVector1(jointNumbersRecaptured, (*it).jointNumbersAnimated)) it = sMotionStates[avatar->getID()].erase(it); else it++; @@ -53,7 +54,7 @@ void FSPoseState::updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingM if (!motion) continue; - if (!posingMotion->otherMotionAnimatesJoints(motion, jointNamesRecaptured)) + if (!posingMotion->otherMotionAnimatesJoints(motion, jointNumbersRecaptured)) continue; bool foundMatch = false; @@ -71,45 +72,17 @@ void FSPoseState::updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingM continue; fsMotionState newState; - newState.motionId = anim_it->first; - newState.lastUpdateTime = motion->getLastUpdateTime(); - newState.jointNamesAnimated = jointNamesRecaptured; - newState.captureOrder = sCaptureOrder[avatar->getID()]; - newState.avatarOwnsPose = canSaveMotionId(avatar, anim_it->first); + newState.motionId = anim_it->first; + newState.lastUpdateTime = motion->getLastUpdateTime(); + newState.jointNumbersAnimated = jointNumbersRecaptured; + newState.captureOrder = sCaptureOrder[avatar->getID()]; + newState.inLayerOrder = animNumber++; + newState.gAgentOwnsPose = canSaveMotionId(avatar, anim_it->first); sMotionStates[avatar->getID()].push_back(newState); } } -bool FSPoseState::addOrUpdatePosingMotionState(LLVOAvatar* avatar, LLUUID animId, F32 updateTime, std::string jointNames, int captureOrder) -{ - if (!avatar) - return false; - - bool foundMatch = false; - for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); it++) - { - bool motionIdMatches = (*it).motionId == animId; - bool updateTimesMatch = (*it).lastUpdateTime == updateTime; - bool jointNamesMatch = (*it).jointNamesAnimated == jointNames; - bool captureOrdersMatch = (*it).captureOrder == captureOrder; - - foundMatch = motionIdMatches && updateTimesMatch && jointNamesMatch && captureOrdersMatch; - if (foundMatch) - return false; - } - - fsMotionState newState; - newState.motionId = animId; - newState.lastUpdateTime = updateTime; - newState.jointNamesAnimated = jointNames; - newState.captureOrder = captureOrder; - newState.avatarOwnsPose = false; - - sMotionStates[avatar->getID()].push_back(newState); - return true; -} - void FSPoseState::purgeMotionStates(LLVOAvatar* avatar) { if (!avatar) @@ -118,7 +91,7 @@ void FSPoseState::purgeMotionStates(LLVOAvatar* avatar) sMotionStates[avatar->getID()].clear(); } -void FSPoseState::writeMotionStates(LLVOAvatar* avatar, LLSD* saveRecord) +void FSPoseState::writeMotionStates(LLVOAvatar* avatar, bool ignoreOwnership, LLSD* saveRecord) { if (!avatar) return; @@ -126,18 +99,27 @@ void FSPoseState::writeMotionStates(LLVOAvatar* avatar, LLSD* saveRecord) int animNumber = 0; for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); ++it) { - if (!it->avatarOwnsPose) - continue; + if (!ignoreOwnership && !it->gAgentOwnsPose) + { + if (it->requeriedAssetInventory) + continue; - std::string uniqueAnimId = "poseState" + std::to_string(animNumber++); - (*saveRecord)[uniqueAnimId]["animationId"] = it->motionId.asString(); - (*saveRecord)[uniqueAnimId]["lastUpdateTime"] = it->lastUpdateTime; - (*saveRecord)[uniqueAnimId]["jointNamesAnimated"] = it->jointNamesAnimated; - (*saveRecord)[uniqueAnimId]["captureOrder"] = it->captureOrder; + it->gAgentOwnsPose = canSaveMotionId(avatar, it->motionId); + it->requeriedAssetInventory = true; + if (!it->gAgentOwnsPose) + continue; + } + + std::string uniqueAnimId = "poseState" + std::to_string(animNumber++); + (*saveRecord)[uniqueAnimId]["animationId"] = it->motionId.asString(); + (*saveRecord)[uniqueAnimId]["lastUpdateTime"] = it->lastUpdateTime; + (*saveRecord)[uniqueAnimId]["jointNumbersAnimated"] = encodeVectorToString(it->jointNumbersAnimated); + (*saveRecord)[uniqueAnimId]["captureOrder"] = it->captureOrder; + (*saveRecord)[uniqueAnimId]["inLayerOrder"] = it->inLayerOrder; } } -void FSPoseState::restoreMotionStates(LLVOAvatar* avatar, LLSD pose) +void FSPoseState::restoreMotionStates(LLVOAvatar* avatar, bool ignoreOwnership, LLSD pose) { if (!avatar) return; @@ -153,25 +135,33 @@ void FSPoseState::restoreMotionStates(LLVOAvatar* avatar, LLSD pose) continue; fsMotionState newState; - newState.avatarOwnsPose = true; if (control_map.has("animationId")) { std::string const name = control_map["animationId"].asString(); LLUUID animId; if (LLUUID::parseUUID(name, &animId)) + { newState.motionId = animId; + newState.gAgentOwnsPose = ignoreOwnership || canSaveMotionId(avatar, animId); + + if (ignoreOwnership) + sMotionStatesOwnedByMe[animId] = true; + } } if (control_map.has("lastUpdateTime")) newState.lastUpdateTime = (F32)control_map["lastUpdateTime"].asReal(); - if (control_map.has("jointNamesAnimated")) - newState.jointNamesAnimated = control_map["jointNamesAnimated"].asString(); + if (control_map.has("jointNumbersAnimated")) + newState.jointNumbersAnimated = decodeStringToVector(control_map["jointNumbersAnimated"].asString()); if (control_map.has("captureOrder")) newState.captureOrder = control_map["captureOrder"].asInteger(); + if (control_map.has("inLayerOrder")) + newState.inLayerOrder = control_map["inLayerOrder"].asInteger(); + if (newState.captureOrder > sCaptureOrder[avatar->getID()]) sCaptureOrder[avatar->getID()] = newState.captureOrder; @@ -207,7 +197,7 @@ bool FSPoseState::applyMotionStatesToPosingMotion(LLVOAvatar* avatar, FSPosingMo resetPriorityForCaptureOrder(avatar, posingMotion, lastCaptureOrder); } - it->motionApplied = posingMotion->loadOtherMotionToBaseOfThisMotion(kfm, it->lastUpdateTime, it->jointNamesAnimated); + it->motionApplied = posingMotion->loadOtherMotionToBaseOfThisMotion(kfm, it->lastUpdateTime, it->jointNumbersAnimated); } else { @@ -225,26 +215,46 @@ void FSPoseState::resetPriorityForCaptureOrder(LLVOAvatar* avatar, FSPosingMotio { for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); it++) { - if (it->jointNamesAnimated.empty()) + if (it->jointNumbersAnimated.empty()) continue; if (it->motionApplied) continue; if (it->captureOrder != captureOrder) continue; - posingMotion->resetBonePriority(it->jointNamesAnimated); + posingMotion->resetBonePriority(it->jointNumbersAnimated); } } -bool FSPoseState::canSaveMotionId(LLVOAvatar* avatar, LLAssetID motionId) +bool FSPoseState::canSaveMotionId(LLVOAvatar* avatarPlayingMotionId, LLAssetID motionId) { if (!gAgentAvatarp || gAgentAvatarp.isNull()) return false; + if (sMotionStatesOwnedByMe[motionId]) + return true; + // does the animation exist in inventory LLInventoryItem* item = gInventory.getItem(motionId); - if (item && item->getPermissions().getOwner() == avatar->getID()) - return true; + if (item && item->getPermissions().getOwner() == gAgentAvatarp->getID()) + { + sMotionStatesOwnedByMe[motionId] = true; + return sMotionStatesOwnedByMe[motionId]; + } + + if (!avatarPlayingMotionId) + return false; + + if (avatarPlayingMotionId->getID() == gAgentAvatarp->getID()) + return motionIdIsAgentAnimationSource(motionId); + + return motionIdIsFromPrimAgentOwnsAgentIsSittingOn(avatarPlayingMotionId, motionId); +} + +bool FSPoseState::motionIdIsAgentAnimationSource(LLAssetID motionId) +{ + if (!gAgentAvatarp || gAgentAvatarp.isNull()) + return false; for (const auto& [anim_object_id, anim_anim_id] : gAgentAvatarp->mAnimationSources) { @@ -252,17 +262,150 @@ bool FSPoseState::canSaveMotionId(LLVOAvatar* avatar, LLAssetID motionId) continue; // is the item that started the anim in inventory - item = gInventory.getItem(anim_object_id); - if (item && item->getPermissions().getOwner() == avatar->getID()) - return true; + LLInventoryItem* item = gInventory.getItem(anim_object_id); + if (item && item->getPermissions().getOwner() == gAgentAvatarp->getID()) + { + sMotionStatesOwnedByMe[motionId] = true; + return sMotionStatesOwnedByMe[motionId]; + } // is the item that start the animation in-world LLViewerObject* object = gObjectList.findObject(anim_object_id); if (object && object->permYouOwner()) - return true; - - return false; + { + sMotionStatesOwnedByMe[motionId] = true; + return sMotionStatesOwnedByMe[motionId]; + } } return false; } + +bool FSPoseState::motionIdIsFromPrimAgentOwnsAgentIsSittingOn(LLVOAvatar* avatarPlayingMotionId, LLAssetID motionId) +{ + if (!avatarPlayingMotionId) + return false; + + const LLViewerObject* agentRoot = dynamic_cast(avatarPlayingMotionId->getRoot()); + if (!agentRoot) + return false; + + const LLUUID& assetIdTheyAreSittingOn = agentRoot->getID(); + if (assetIdTheyAreSittingOn == avatarPlayingMotionId->getID()) + return false; // they are not sitting on a thing + + LLViewerObject* object = gObjectList.findObject(assetIdTheyAreSittingOn); + if (!object || !object->permYouOwner()) + return false; // gAgent does not own what they are sitting on + + if (object->isInventoryPending()) + return false; + + if (object->isInventoryDirty() || !object->getInventoryRoot()) + { + object->requestInventory(); + return false; // whatever they are sitting on, we don't have the inventory list for yet + } + + LLInventoryItem* item = object->getInventoryItemByAsset(motionId, LLAssetType::AT_ANIMATION); + if (item && item->getPermissions().getOwner() == gAgentAvatarp->getID()) + { + sMotionStatesOwnedByMe[motionId] = true; + return sMotionStatesOwnedByMe[motionId]; + } + + return false; +} + +bool FSPoseState::vector2IsSubsetOfVector1(std::vector newRecapture, std::vector oldRecapture) +{ + if (newRecapture.size() < 1) + return false; + if (oldRecapture.size() < 1) + return false; + + if (newRecapture.size() < oldRecapture.size()) + return false; + + for (S32 number : oldRecapture) + if (std::find(newRecapture.begin(), newRecapture.end(), number) == newRecapture.end()) + return false; + + return true; +} + +std::string FSPoseState::encodeVectorToString(std::vector vector) +{ + std::string encoded = ""; + if (vector.size() < 1) + return encoded; + + for (S32 numberToEncode : vector) + { + if (numberToEncode > 251) // max 216 at time of writing + continue; + + S32 number = numberToEncode; + + if (number >= 189) + { + encoded += "~"; + number -= 189; + } + + if (number >= 126) + { + encoded += "}"; + number -= 126; + } + + if (number >= 63) + { + encoded += "|"; + number -= 63; + } + + encoded += char(number + int('?')); + } + + return encoded; +} + +std::vector FSPoseState::decodeStringToVector(std::string vector) +{ + std::vector decoded; + if (vector.empty()) + return decoded; + + S32 number = 0; + for (char ch : vector) + { + if (ch > '~' || ch < '?') + continue; + + if (ch == '~') + { + number += 189; + continue; + } + + if (ch == '}') + { + number += 126; + continue; + } + + if (ch == '|') + { + number += 63; + continue; + } + + number -= int('?'); + number += S32(ch); + decoded.push_back(number); + number = 0; + } + + return decoded; +} diff --git a/indra/newview/fsposestate.h b/indra/newview/fsposestate.h index e941e28d70..0f68459aee 100644 --- a/indra/newview/fsposestate.h +++ b/indra/newview/fsposestate.h @@ -49,18 +49,7 @@ public: /// The avatar whose animations are to be captured. /// The posing motion. /// The names of the joints being recaptured. - void updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingMotion, std::string jointNamesRecaptured); - - /// - /// Add a new posing state, or updates the matching posing state with the supplied data. - /// - /// The avatar the posing state is intended for. - /// The ID of the animation. - /// The frame-time of the animation. - /// The names of the joints, if any, the animation should specifically be applied to. - /// The capture order. - /// True if the posing state was added or changed by the update data, otherwise false. - bool addOrUpdatePosingMotionState(LLVOAvatar* avatar, LLUUID animId, F32 updateTime, std::string jointNames, int captureOrder); + void updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingMotion, std::vector jointNamesRecaptured); /// /// Removes all current animation states for the supplied avatar. @@ -72,16 +61,17 @@ public: /// Writes any documented poses for the supplied avatar to the supplied stream. /// /// The avatar whose animations may have been captured. + /// Whether to ignore ownership. For use when preparing saveRecord to send to another by collab. /// The record to add to. - void writeMotionStates(LLVOAvatar* avatar, LLSD* saveRecord); + void writeMotionStates(LLVOAvatar* avatar, bool ignoreOwnership, LLSD* saveRecord); /// /// Restores pose state(s) from the supplied record. /// /// The avatar whose animations may have been captured. - /// The posing motion. + /// Whether to ignore ownership. For use when reading a local file. /// The record to read from. - void restoreMotionStates(LLVOAvatar* avatar, LLSD pose); + void restoreMotionStates(LLVOAvatar* avatar, bool ignoreOwnership, LLSD pose); /// /// Applies the motion states for the supplied avatar to the supplied motion. @@ -122,20 +112,30 @@ private: bool motionApplied = false; /// - /// Whether the avatar owns the pose, or the pose was loaded. + /// For non-gAgent, we permit a query of the inventory of a prim they are sitting on. + /// Because this involves latency, we may retry ownership checking at save-time. /// - bool avatarOwnsPose = false; + bool requeriedAssetInventory = false; /// - /// When reloading, larger numbers are loaded last, nesting order and priority. - /// This is used to represent recaptures, where joints could be animated with different poses. + /// Whether gAgent owns the pose, or the pose was loaded from XML. + /// + bool gAgentOwnsPose = false; + + /// + /// Represents 'capture layers: how the user layers animations 'on top of' others. /// int captureOrder = 0; /// - /// When reloading, and if not-empty, the names of the bones this motionId should affect. + /// Represents in-layer order of capture. /// - std ::string jointNamesAnimated; + int inLayerOrder = 0; + + /// + /// When reloading, and if not-empty, the bone-numbers this motionId should affect. + /// + std ::vector jointNumbersAnimated; }; /// @@ -147,12 +147,58 @@ private: void resetPriorityForCaptureOrder(LLVOAvatar* avatar, FSPosingMotion* posingMotion, int captureOrder); /// - /// Gets whether the supplied avatar owns, and thus can save information about the supplied asset ID. + /// Gets whether gAgentID owns, and thus can save information about the supplied motionId. /// - /// The avatar to query ownership for. - /// The asset ID of the object. - /// True if the avatar owns the asset, otherwise false. - bool canSaveMotionId(LLVOAvatar* avatar, LLAssetID motionId); + /// The avatar playing the supplied motionId. + /// The motionId of the animation. + /// True if the gAgent owns the motionId, otherwise false. + /// + /// This only works reliably for self. + /// For motions playing on others, the motion needs to be an asset in gAgent's inventory. + /// + bool canSaveMotionId(LLVOAvatar* avatarPlayingMotionId, LLAssetID motionId); + + /// + /// Examines gAgent's animation source list for the supplied animation Id. + /// + /// The ID of the motion to query. + /// True if gAgent is playing the animation, otherwise false. + bool motionIdIsAgentAnimationSource(LLAssetID motionId); + + /// + /// Queries a specific condition of the supplied animation ID. + /// + /// The avatar to query for. + /// The motion ID to query for. + /// + /// True if the supplied avatar is sitting on an object owned by gAgent, and that object + /// contains an animation asset with the same assetId. + /// + /// + /// This is intended to test for a situation a photographer might arrange. + /// If you are sitting on photographer's prim, playing photographer's pose, and photographer wants to save their work, + /// this allows them to save the Animation ID and state to XML. + /// It is intended this be called twice at least, as it does not implement a callback onInventoryLoaded. + /// Presently this works fine: first time being when posing starts, second when pose is saved. + /// + bool motionIdIsFromPrimAgentOwnsAgentIsSittingOn(LLVOAvatar* avatarPlayingMotionId, LLAssetID motionId); + + /// + /// Tests if all the members of supplied vector2 are members of supplied vector1. + /// + /// The super-set. + /// The possible sub-set. + /// True if all members of vector2 are members of vector1, otherwise false. + bool vector2IsSubsetOfVector1(std::vector vector1, std::vector vector2); + + /// + /// Two symmetric methods for (de)serializing vectors to both XML and collab-safe short-as-possible strings and back again. + /// + /// + /// Collab-safe means ASCII-printable chars, and delimiter usage does not conflict with Collab's delimiter. + /// + std::string encodeVectorToString(std::vector vector); + std::vector decodeStringToVector(std::string vector); struct compareByCaptureOrder { @@ -160,6 +206,8 @@ private: { if (a.captureOrder < b.captureOrder) return true; // Ascending order + if (a.captureOrder == b.captureOrder && a.inLayerOrder < b.inLayerOrder) + return true; // Ascending order in layer return false; } @@ -167,6 +215,7 @@ private: static std::map > sMotionStates; static std::map sCaptureOrder; + static std::map sMotionStatesOwnedByMe; }; #endif // LL_FSPoseState_H diff --git a/indra/newview/fsposingmotion.cpp b/indra/newview/fsposingmotion.cpp index 4d9a51b8b3..e34af7943a 100644 --- a/indra/newview/fsposingmotion.cpp +++ b/indra/newview/fsposingmotion.cpp @@ -208,7 +208,7 @@ void FSPosingMotion::setJointState(LLJoint* joint, U32 state) FSJointPose* FSPosingMotion::getJointPoseByJointName(const std::string& name) { - if (mJointPoses.size() < 1) + if (name.empty() || mJointPoses.size() < 1) return nullptr; for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) @@ -222,6 +222,24 @@ FSJointPose* FSPosingMotion::getJointPoseByJointName(const std::string& name) return nullptr; } +FSJointPose* FSPosingMotion::getJointPoseByJointNumber(const S32& number) +{ + if (mJointPoses.size() < 1) + return nullptr; + if (number < 0) + return nullptr; + + for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) + { + if (poserJoint_iter->getJointNumber() != number) + continue; + + return &*poserJoint_iter; + } + + return nullptr; +} + bool FSPosingMotion::currentlyPosingJoint(LLJoint* joint) { if (mJointPoses.size() < 1) @@ -270,14 +288,14 @@ void FSPosingMotion::setJointBvhLock(FSJointPose* joint, bool lockInBvh) joint->zeroBaseRotation(lockInBvh); } -bool FSPosingMotion::loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionToLoad, F32 timeToLoadAt, std::string selectedJointNames) +bool FSPosingMotion::loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionToLoad, F32 timeToLoadAt, std::vector selectedJointNumbers) { FSPosingMotion* motionToLoadAsFsPosingMotion = static_cast(motionToLoad); if (!motionToLoadAsFsPosingMotion) return false; LLJoint::JointPriority priority = motionToLoad->getPriority(); - bool motionIsForAllJoints = selectedJointNames.empty(); + bool motionIsForAllJoints = selectedJointNumbers.empty(); LLQuaternion rot; LLVector3 position, scale; @@ -285,16 +303,18 @@ bool FSPosingMotion::loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionT for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) { - std::string jointName = poserJoint_iter->jointName(); + S32 jointNumber = poserJoint_iter->getJointNumber(); + std::string jointName = poserJoint_iter->jointName(); - bool motionIsForThisJoint = selectedJointNames.find(jointName) != std::string::npos; + bool motionIsForThisJoint = + std::find(selectedJointNumbers.begin(), selectedJointNumbers.end(), jointNumber) != selectedJointNumbers.end(); if (!motionIsForAllJoints && !motionIsForThisJoint) continue; hasRotation = hasPosition = hasScale = false; motionToLoadAsFsPosingMotion->getJointStateAtTime(jointName, timeToLoadAt, &hasRotation, &rot, &hasPosition, &position, &hasScale, &scale); - if (hasRotation) + if (hasRotation && !poserJoint_iter->userHasSetBaseRotationToZero()) poserJoint_iter->setBaseRotation(rot, priority); if (hasPosition) @@ -337,16 +357,17 @@ void FSPosingMotion::getJointStateAtTime(std::string jointPoseName, F32 timeToLo } } -bool FSPosingMotion::otherMotionAnimatesJoints(LLKeyframeMotion* motionToQuery, std::string recapturedJointNames) +bool FSPosingMotion::otherMotionAnimatesJoints(LLKeyframeMotion* motionToQuery, std::vector recapturedJointNumbers) { FSPosingMotion* motionToLoadAsFsPosingMotion = static_cast(motionToQuery); if (!motionToLoadAsFsPosingMotion) return false; - return motionToLoadAsFsPosingMotion->motionAnimatesJoints(recapturedJointNames); + return motionToLoadAsFsPosingMotion->motionAnimatesJoints(recapturedJointNumbers); } -bool FSPosingMotion::motionAnimatesJoints(std::string recapturedJointNames) +// Do not try to access FSPosingMotion state; you are a LLKeyframeMotion cast as a FSPosingMotion, NOT an FSPosingMotion. +bool FSPosingMotion::motionAnimatesJoints(std::vector recapturedJointNumbers) { if (mJointMotionList == nullptr) return false; @@ -354,7 +375,9 @@ bool FSPosingMotion::motionAnimatesJoints(std::string recapturedJointNames) for (U32 i = 0; i < mJointMotionList->getNumJointMotions(); i++) { JointMotion* jm = mJointMotionList->getJointMotion(i); - if (recapturedJointNames.find(jm->mJointName) == std::string::npos) + LLJoint* joint = mCharacter->getJoint(jm->mJointName); + + if (std::find(recapturedJointNumbers.begin(), recapturedJointNumbers.end(), joint->getJointNum()) == recapturedJointNumbers.end()) continue; if (jm->mRotationCurve.mNumKeys > 0) @@ -364,16 +387,15 @@ bool FSPosingMotion::motionAnimatesJoints(std::string recapturedJointNames) return false; } -void FSPosingMotion::resetBonePriority(std::string boneNamesToReset) +void FSPosingMotion::resetBonePriority(std::vector boneNumbersToReset) { - if (boneNamesToReset.empty()) - return; - - for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) + for (S32 boneNumber : boneNumbersToReset) { - std::string jointName = poserJoint_iter->jointName(); - if (boneNamesToReset.find(jointName) != std::string::npos) - poserJoint_iter->setJointPriority(LLJoint::LOW_PRIORITY); + for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) + { + if (poserJoint_iter->getJointNumber() == boneNumber) + poserJoint_iter->setJointPriority(LLJoint::LOW_PRIORITY); + } } } diff --git a/indra/newview/fsposingmotion.h b/indra/newview/fsposingmotion.h index 0f336b3ba8..1db88e8f86 100644 --- a/indra/newview/fsposingmotion.h +++ b/indra/newview/fsposingmotion.h @@ -108,6 +108,13 @@ public: /// The matching joint pose, if found, otherwise null. FSJointPose* getJointPoseByJointName(const std::string& name); + /// + /// Gets the joint pose by its LLJoint number. + /// + /// The number of the joint to get the pose for. + /// The matching joint pose, if found, otherwise null. + FSJointPose* getJointPoseByJointNumber(const S32& number); + /// /// Gets the motion identity for this animation. /// @@ -139,9 +146,9 @@ public: /// /// The motion whose joint rotations (etc) we want to copy to this. /// The play-time the animation should be advanced to derive the correct joint state. - /// If only some of the joints should be animated by this motion, name them here. + /// If only some of the joints should be animated by this motion, number them here. /// - bool loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionToLoad, F32 timeToLoadAt, std::string selectedJointNames); + bool loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionToLoad, F32 timeToLoadAt, std::vector selectedJointNumbers); /// /// Tries to get the rotation, position and scale for the supplied joint name at the supplied time. @@ -164,27 +171,27 @@ public: /// /// Resets the bone priority to zero for the joints named in the supplied string. /// - /// The string containg bone names (like mPelvis). - void resetBonePriority(std::string boneNamesToReset); + /// The vector containing bone numbers. + void resetBonePriority(std::vector boneNumbersToReset); /// /// Queries whether the supplied motion animates any of the joints named in the supplied string. /// /// The motion to query. - /// A string containing all of the joint names. + /// A string containing all of the joint numbers. /// True if the motion animates any of the bones named, otherwise false. - bool otherMotionAnimatesJoints(LLKeyframeMotion* motionToQuery, std::string recapturedJointNames); + bool otherMotionAnimatesJoints(LLKeyframeMotion* motionToQuery, std::vector recapturedJointNumbers); /// /// Queries whether the this motion animates any of the joints named in the supplied string. /// - /// A string containing all of the joint names. + /// A vector containing all of the joint numbers this motion animates. /// True if the motion animates any of the bones named, otherwise false. /// /// The most significant thing this method does is provide access to protected properties of an LLPosingMotion. /// Thus its most common usage would be to access those properties for an arbitrary animation. /// - bool motionAnimatesJoints(std::string recapturedJointNames); + bool motionAnimatesJoints(std::vector recapturedJointNumbers); private: /// diff --git a/indra/newview/lltoolcomp.h b/indra/newview/lltoolcomp.h index fbce6f9079..04364b45e4 100644 --- a/indra/newview/lltoolcomp.h +++ b/indra/newview/lltoolcomp.h @@ -272,7 +272,8 @@ public: 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 ); }; + void setJoint(LLJoint* joint) { mManip->setJoint(joint); }; + void setReferenceFrame(E_PoserReferenceFrame frame) { mManip->setReferenceFrame(frame); }; // Optional override if you have SHIFT/CTRL combos virtual LLTool* getOverrideTool(MASK mask) override; 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 f74b357403..3ab8140f0a 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -83,8 +83,6 @@ width="430"> SWAP_YAW_AND_ROLL NEGATE_PITCH SWAP_YAW_AND_ROLL NEGATE_PITCH SWAP_YAW_AND_ROLL NEGATE_PITCH - SWAP_YAW_AND_ROLL NEGATE_PITCH - SWAP_YAW_AND_ROLL NEGATE_PITCH SWAP_ROLL_AND_PITCH SWAP_ROLL_AND_PITCH NEGATE_PITCH SWAP_ROLL_AND_PITCH @@ -160,6 +158,105 @@ width="430"> SWAP_YAW_AND_ROLL NEGATE_PITCH SWAP_YAW_AND_ROLL NEGATE_PITCH + + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + NEGATE_ROLL NEGATE_PITCH + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + SWAP_YAW_AND_ROLL NEGATE_ALL + true true @@ -416,7 +513,7 @@ width="430"> layout="topleft" mouse_opaque="false" left="5" - name="title" + name="move_tab_panel" top="0" width="235"> name="refresh_avatars" tool_tip="Refresh the list of avatars and animeshes" width="20" - top="232" + top="221" left="3"> @@ -894,7 +991,7 @@ width="430"> flash_color="0.7 0.7 1 1" button_flash_count="64" button_flash_rate="0.5" - tool_tip="Start posing the selected avatar or animesh, if you are allowed to" + tool_tip="Start posing the selected avatar or animesh, if you are allowed to." name="start_stop_posing_button" width="150"> label="Show joint markers" follows="left|top" left="5" - tool_tip="Show small indicators to aid joint selection when visually posing." + tool_tip="Show small indicators to aid joint selection when posing." top_pad="5" width="134" /> + + +