FIRE-30873: Rework undo, add redo

Now affected by mirror/symp, undo is now joint specific, and has undo-depth of last 20 changes
master
Angeldark Raymaker 2024-10-05 19:45:18 +01:00
parent da998c838e
commit 542dc37c97
9 changed files with 351 additions and 63 deletions

View File

@ -69,6 +69,7 @@ static const std::string POSER_AVATAR_SLIDER_ROLL_NAME = "limb_roll"; // your ea
static const std::string POSER_AVATAR_TOGGLEBUTTON_TRACKPADSENSITIVITY = "button_toggleTrackPadSensitivity";
static const std::string POSER_AVATAR_TOGGLEBUTTON_MIRROR = "button_toggleMirrorRotation";
static const std::string POSER_AVATAR_TOGGLEBUTTON_SYMPATH = "button_toggleSympatheticRotation";
static const std::string POSER_AVATAR_BUTTON_REDO = "button_redo_change";
static const std::string POSER_AVATAR_SLIDER_POSX_NAME = "av_position_inout";
static const std::string POSER_AVATAR_SLIDER_POSY_NAME = "av_position_leftright";
static const std::string POSER_AVATAR_SLIDER_POSZ_NAME = "av_position_updown";
@ -118,6 +119,7 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key)
mCommitCallbackRegistrar.add("Poser.ToggleAdvancedPanel", boost::bind(&FSFloaterPoser::onToggleAdvancedPanel, this));
mCommitCallbackRegistrar.add("Poser.UndoLastRotation", boost::bind(&FSFloaterPoser::onUndoLastRotation, this));
mCommitCallbackRegistrar.add("Poser.RedoLastRotation", boost::bind(&FSFloaterPoser::onRedoLastRotation, this));
mCommitCallbackRegistrar.add("Poser.ToggleMirrorChanges", boost::bind(&FSFloaterPoser::onToggleMirrorChange, this));
mCommitCallbackRegistrar.add("Poser.ToggleSympatheticChanges", boost::bind(&FSFloaterPoser::onToggleSympatheticChange, this));
mCommitCallbackRegistrar.add("Poser.ToggleTrackPadSensitivity", boost::bind(&FSFloaterPoser::refreshTrackpadCursor, this));
@ -936,12 +938,67 @@ void FSFloaterPoser::setRotationChangeButtons(bool togglingMirror, bool toggling
void FSFloaterPoser::onUndoLastRotation()
{
FSVirtualTrackpad *trackBall = getChild<FSVirtualTrackpad>(POSER_AVATAR_TRACKBALL_NAME);
if (!trackBall)
LLVOAvatar* avatar = getUiSelectedAvatar();
if (!avatar)
return;
trackBall->undoLastValue();
onLimbTrackballChanged();
if (!_poserAnimator.isPosingAvatar(avatar))
return;
auto selectedJoints = getUiSelectedPoserJoints();
if (selectedJoints.size() < 1)
return;
bool shouldEnableRedoButton = false;
for (auto item : selectedJoints)
{
bool currentlyPosing = _poserAnimator.isPosingAvatarJoint(avatar, *item);
if (currentlyPosing)
_poserAnimator.undoLastJointRotation(avatar, *item, getUiSelectedBoneDeflectionStyle());
shouldEnableRedoButton |= _poserAnimator.canRedoJointRotation(avatar, *item);
}
enableOrDisableRedoButton(shouldEnableRedoButton);
refreshRotationSliders();
refreshTrackpadCursor();
}
void FSFloaterPoser::onRedoLastRotation()
{
LLVOAvatar* avatar = getUiSelectedAvatar();
if (!avatar)
return;
if (!_poserAnimator.isPosingAvatar(avatar))
return;
auto selectedJoints = getUiSelectedPoserJoints();
if (selectedJoints.size() < 1)
return;
bool shouldEnableRedoButton = false;
for (auto item : selectedJoints)
{
bool currentlyPosing = _poserAnimator.isPosingAvatarJoint(avatar, *item);
if (currentlyPosing)
_poserAnimator.redoLastJointRotation(avatar, *item, getUiSelectedBoneDeflectionStyle());
shouldEnableRedoButton |= _poserAnimator.canRedoJointRotation(avatar, *item);
}
enableOrDisableRedoButton(shouldEnableRedoButton);
refreshRotationSliders();
refreshTrackpadCursor();
}
void FSFloaterPoser::enableOrDisableRedoButton(bool shouldEnable)
{
LLButton* redoButton = getChild<LLButton>(POSER_AVATAR_BUTTON_REDO);
if (!redoButton)
return;
redoButton->setEnabled(shouldEnable);
}
void FSFloaterPoser::onOpenSetAdvancedPanel()

View File

@ -210,6 +210,8 @@ class FSFloaterPoser : public LLFloater
void onToggleSympatheticChange();
void setRotationChangeButtons(bool mirror, bool sympathetic);
void onUndoLastRotation();
void onRedoLastRotation();
void enableOrDisableRedoButton(bool shouldEnable);
void onPoseStartStop();
void onLimbTrackballChanged();
void onLimbYawPitchRollChanged();

View File

@ -92,11 +92,87 @@ void FSPoserAnimator::resetAvatarJoint(LLVOAvatar *avatar, FSPoserJoint joint)
FSPosingMotion::FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName());
if (!jointPose)
return;
jointPose->setTargetPosition(jointPose->getBeginningPosition());
jointPose->setTargetRotation(jointPose->getBeginningRotation());
}
void FSPoserAnimator::undoLastJointRotation(LLVOAvatar* avatar, FSPoserJoint joint, E_BoneDeflectionStyles style)
{
if (!isAvatarSafeToUse(avatar))
return;
FSPosingMotion* posingMotion = getPosingMotion(avatar);
if (!posingMotion)
return;
if (posingMotion->isStopped())
return;
FSPosingMotion::FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName());
if (!jointPose)
return;
jointPose->undoLastRotationSet();
if (style == NONE)
return;
FSPosingMotion::FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint.mirrorJointName());
if (!oppositeJointPose)
return;
oppositeJointPose->undoLastRotationSet();
}
bool FSPoserAnimator::canRedoJointRotation(LLVOAvatar* avatar, FSPoserJoint joint)
{
if (!isAvatarSafeToUse(avatar))
return false;
FSPosingMotion* posingMotion = getPosingMotion(avatar);
if (!posingMotion)
return false;
if (posingMotion->isStopped())
return false;
FSPosingMotion::FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName());
if (!jointPose)
return false;
return jointPose->canRedo();
}
void FSPoserAnimator::redoLastJointRotation(LLVOAvatar* avatar, FSPoserJoint joint, E_BoneDeflectionStyles style)
{
if (!isAvatarSafeToUse(avatar))
return;
FSPosingMotion* posingMotion = getPosingMotion(avatar);
if (!posingMotion)
return;
if (posingMotion->isStopped())
return;
FSPosingMotion::FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName());
if (!jointPose)
return;
jointPose->redoLastRotationSet();
if (style == NONE)
return;
FSPosingMotion::FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint.mirrorJointName());
if (!oppositeJointPose)
return;
oppositeJointPose->redoLastRotationSet();
}
LLVector3 FSPoserAnimator::getJointPosition(LLVOAvatar *avatar, FSPoserJoint joint)
{
LLVector3 pos;

View File

@ -252,6 +252,28 @@ public:
/// <param name="joint">The joint to be reset.</param>
void resetAvatarJoint(LLVOAvatar *avatar, FSPoserJoint joint);
/// <summary>
/// Undoes the last applied rotation to the supplied PoserJoint.
/// </summary>
/// <param name="avatar">The avatar having the joint to which we refer.</param>
/// <param name="joint">The joint with the rotation to undo.</param>
void undoLastJointRotation(LLVOAvatar* avatar, FSPoserJoint joint, E_BoneDeflectionStyles style);
/// <summary>
/// Determines if a redo action is currently permitted for the supplied joint.
/// </summary>
/// <param name="avatar">The avatar having the joint to which we refer.</param>
/// <param name="joint">The joint to query.</param>
/// <returns>True if a redo action is available, otherwise false.</returns>
bool canRedoJointRotation(LLVOAvatar* avatar, FSPoserJoint joint);
/// <summary>
/// Re-does the last undone rotation to the supplied PoserJoint.
/// </summary>
/// <param name="avatar">The avatar having the joint to which we refer.</param>
/// <param name="joint">The joint with the rotation to redo.</param>
void redoLastJointRotation(LLVOAvatar* avatar, FSPoserJoint joint, E_BoneDeflectionStyles style);
/// <summary>
/// Gets the position of a joint for the supplied avatar.
/// </summary>

View File

@ -63,7 +63,6 @@ bool FSPosingMotion::onUpdate(F32 time, U8* joint_mask)
LLQuaternion currentRotation;
LLVector3 currentPosition;
LLVector3 targetPosition;
F32 poseTransitionAmount = 0.0f; // when we change from one position/rotation to another, we do so over time; this documents the amount of transition.
for (FSJointPose jointPose : _jointPoses)
{
@ -76,7 +75,6 @@ bool FSPosingMotion::onUpdate(F32 time, U8* joint_mask)
targetRotation = jointPose.getTargetRotation();
targetPosition = jointPose.getTargetPosition();
poseTransitionAmount = llclamp(_interpolationTimer.getElapsedTimeF32() / _interpolationTime, 0.0f, 1.0f);
if (currentPosition != targetPosition)
{
currentPosition = lerp(currentPosition, targetPosition, _interpolationTime);
@ -90,9 +88,6 @@ bool FSPosingMotion::onUpdate(F32 time, U8* joint_mask)
}
}
if (_interpolationTimer.getStarted() && poseTransitionAmount >= 1.0f)
_interpolationTimer.stop();
return true;
}

View File

@ -24,8 +24,8 @@
* $/LicenseInfo$
*/
#ifndef LL_FSPOSINGMOTION_H
#define LL_FSPOSINGMOTION_H
#ifndef FS_POSINGMOTION_H
#define FS_POSINGMOTION_H
//-----------------------------------------------------------------------------
// Header files
@ -52,12 +52,65 @@ public:
/// </summary>
class FSJointPose
{
std::string _jointName = ""; // expected to be a match to LLJoint.getName() for a joint implementation.
LLQuaternion _beginningRotation;
LLQuaternion _targetRotation;
const size_t MaximumUndoQueueLength = 20;
/// <summary>
/// The constant time interval, in seconds,
/// </summary>
std::chrono::duration<double> const _undoUpdateInterval = std::chrono::duration<double>(0.3);
std::string _jointName = ""; // expected to be a match to LLJoint.getName() for a joint implementation.
LLPointer<LLJointState> _jointState;
LLQuaternion _targetRotation;
LLQuaternion _beginningRotation;
std::deque<LLQuaternion> _lastSetRotations;
size_t _undoneRotationIndex = 0;
std::chrono::system_clock::time_point _timeLastUpdatedRotation = std::chrono::system_clock::now();
LLVector3 _targetPosition;
LLVector3 _beginningPosition;
LLPointer<LLJointState> _jointState;
std::deque<LLVector3> _lastSetPositions;
size_t _undonePositionIndex = 0;
std::chrono::system_clock::time_point _timeLastUpdatedPosition = std::chrono::system_clock::now();
/// <summary>
/// Adds a last position to the deque.
/// </summary>
void addLastPositionToUndo()
{
if (_undonePositionIndex > 0)
{
for (int i = 0; i < _undonePositionIndex; i++)
_lastSetPositions.pop_front();
_undonePositionIndex = 0;
}
_lastSetPositions.push_front(_targetPosition);
while (_lastSetPositions.size() > MaximumUndoQueueLength)
_lastSetPositions.pop_back();
}
/// <summary>
/// Adds a last rotation to the deque.
/// </summary>
void addLastRotationToUndo()
{
if (_undoneRotationIndex > 0)
{
for (int i = 0; i < _undoneRotationIndex; i++)
_lastSetRotations.pop_front();
_undoneRotationIndex = 0;
}
_lastSetRotations.push_front(_targetRotation);
while (_lastSetRotations.size() > MaximumUndoQueueLength)
_lastSetRotations.pop_back();
}
public:
/// <summary>
@ -65,6 +118,8 @@ public:
/// </summary>
std::string jointName() const { return _jointName; }
bool canRedo() const { return _undoneRotationIndex > 0; }
/// <summary>
/// Gets the position the joint was in when the animation was initialized.
/// </summary>
@ -78,7 +133,15 @@ public:
/// <summary>
/// Sets the position the animator wishes the joint to be in.
/// </summary>
void setTargetPosition(const LLVector3& pos) { _targetPosition.set(pos) ; }
void setTargetPosition(const LLVector3& pos)
{
auto timeIntervalSinceLastRotationChange = std::chrono::system_clock::now() - _timeLastUpdatedPosition;
if (timeIntervalSinceLastRotationChange > _undoUpdateInterval)
addLastPositionToUndo();
_timeLastUpdatedPosition = std::chrono::system_clock::now();
_targetPosition.set(pos);
}
/// <summary>
/// Gets the rotation the joint was in when the animation was initialized.
@ -93,7 +156,82 @@ public:
/// <summary>
/// Sets the rotation the animator wishes the joint to be in.
/// </summary>
void setTargetRotation(const LLQuaternion& rot) { _targetRotation.set(rot); }
void setTargetRotation(const LLQuaternion& rot)
{
auto timeIntervalSinceLastRotationChange = std::chrono::system_clock::now() - _timeLastUpdatedRotation;
if (timeIntervalSinceLastRotationChange > _undoUpdateInterval)
addLastRotationToUndo();
_timeLastUpdatedRotation = std::chrono::system_clock::now();
_targetRotation.set(rot);
}
/// <summary>
/// Undoes the last position set, if any.
/// </summary>
void undoLastPositionSet()
{
if (_lastSetPositions.empty())
return;
if (_undonePositionIndex == 0) // at the top of the queue add the current
addLastPositionToUndo();
_undonePositionIndex++;
_undonePositionIndex = llclamp(_undonePositionIndex, 0, _lastSetPositions.size() - 1);
_targetPosition.set(_lastSetPositions[_undonePositionIndex]);
}
/// <summary>
/// Undoes the last position set, if any.
/// </summary>
void redoLastPositionSet()
{
if (_lastSetPositions.empty())
return;
_undonePositionIndex--;
_undonePositionIndex = llclamp(_undonePositionIndex, 0, _lastSetPositions.size() - 1);
_targetPosition.set(_lastSetPositions[_undonePositionIndex]);
if (_undonePositionIndex == 0)
_lastSetRotations.pop_front();
}
/// <summary>
/// Undoes the last rotation set, if any.
/// Ordinarily the queue does not contain the current rotation, because we rely on time to add, and not button-up.
/// When we undo, if we are at the top of the queue, we need to add the current rotation so we can redo back to it.
/// Thus when we start undoing, _undoneRotationIndex points at the current rotation.
/// </summary>
void undoLastRotationSet()
{
if (_lastSetRotations.empty())
return;
if (_undoneRotationIndex == 0) // at the top of the queue add the current
addLastRotationToUndo();
_undoneRotationIndex++;
_undoneRotationIndex = llclamp(_undoneRotationIndex, 0, _lastSetRotations.size() - 1);
_targetRotation.set(_lastSetRotations[_undoneRotationIndex]);
}
/// <summary>
/// Undoes the last rotation set, if any.
/// </summary>
void redoLastRotationSet()
{
if (_lastSetRotations.empty())
return;
_undoneRotationIndex--;
_undoneRotationIndex = llclamp(_undoneRotationIndex, 0, _lastSetRotations.size() - 1);
_targetRotation.set(_lastSetRotations[_undoneRotationIndex]);
if (_undoneRotationIndex == 0)
_lastSetRotations.pop_front();
}
/// <summary>
/// Gets the pointer to the jointstate for the joint this represents.
@ -193,19 +331,16 @@ private:
/// <summary>
/// The amount of time, in seconds, we use for transitioning between one animation-state to another; this affects the 'fluidity'
/// of motion between changed to a joint.
/// Use caution making this larger than the perceptual amount of time between adjusting a joint and then choosing to use 'undo'.
/// Undo-function waits this amount of time after the last user-incited joint change to add a 'restore point'.
/// </summary>
const F32 _interpolationTime = 0.25f;
/// <summary>
/// The timer used to smoothly transition from one joint position or rotation to another.
/// </summary>
LLFrameTimer _interpolationTimer;
/// <summary>
/// The collection of joint poses this motion uses to pose the joints of the character this is animating.
/// </summary>
std::vector<FSJointPose> _jointPoses;
};
#endif // LL_LLKEYFRAMEMOTION_H
#endif // FS_POSINGMOTION_H

View File

@ -57,8 +57,8 @@ FSVirtualTrackpad::FSVirtualTrackpad(const FSVirtualTrackpad::Params &p)
mInfiniteScrollMode(p.infinite_scroll_mode)
{
LLRect border_rect = getLocalRect();
_valueX = _lastValueX = _pinchValueX = _lastPinchValueX = border_rect.getCenterX();
_valueY = _lastValueY = _pinchValueY = _lastPinchValueY = border_rect.getCenterY();
_valueX = _pinchValueX = border_rect.getCenterX();
_valueY = _pinchValueY = border_rect.getCenterY();
_thumbClickOffsetX = _thumbClickOffsetY = _pinchThumbClickOffsetX = _pinchThumbClickOffsetY = 0;
_valueWheelClicks = _pinchValueWheelClicks = 0;
@ -246,10 +246,6 @@ void FSVirtualTrackpad::setValue(F32 x, F32 y) { convertNormalizedToPixelPos(x,
void FSVirtualTrackpad::setPinchValue(F32 x, F32 y) { convertNormalizedToPixelPos(x, y, &_pinchValueX, &_pinchValueY); }
void FSVirtualTrackpad::undoLastValue() { setValueAndCommit(_lastValueX, _lastValueY); }
void FSVirtualTrackpad::undoLastSetPinchValue() { setPinchValueAndCommit(_lastPinchValueX, _lastPinchValueY); }
void FSVirtualTrackpad::setValueAndCommit(const S32 x, const S32 y)
{
_valueX = x;
@ -405,8 +401,6 @@ bool FSVirtualTrackpad::handleMouseDown(S32 x, S32 y, MASK mask)
determineThumbClickError(x, y);
updateClickErrorIfInfiniteScrolling();
_valueWheelClicks = 0;
_lastValueX = _valueX;
_lastValueY = _valueY;
_wheelClicksSinceMouseDown = 0;
gFocusMgr.setMouseCapture(this);
@ -440,8 +434,6 @@ bool FSVirtualTrackpad::handleRightMouseDown(S32 x, S32 y, MASK mask)
determineThumbClickErrorForPinch(x, y);
updateClickErrorIfInfiniteScrollingForPinch();
_pinchValueWheelClicks = 0;
_lastPinchValueX = _pinchValueX;
_lastPinchValueY = _pinchValueY;
_wheelClicksSinceMouseDown = 0;
doingPinchMode = true;
gFocusMgr.setMouseCapture(this);

View File

@ -70,9 +70,6 @@ public:
/// <param name="y">The y-axis (top/bottom) position to set; expected range 1..-1; top = 1</param>
void setValue(F32 x, F32 y);
void undoLastValue();
void undoLastSetPinchValue();
/// <summary>
/// Sets the position of the second cursor.
/// </summary>
@ -140,14 +137,10 @@ private:
S32 _valueX;
S32 _valueY;
S32 _valueWheelClicks;
S32 _lastValueX;
S32 _lastValueY;
S32 _pinchValueX;
S32 _pinchValueY;
S32 _pinchValueWheelClicks;
S32 _lastPinchValueX;
S32 _lastPinchValueY;
/// <summary>
/// Rolling the wheel is pioneering a 'delta' mode: where changes are handled by the control-owner in a relative way.

View File

@ -612,16 +612,47 @@ width="565">
follows="top|left"
layout="topleft"
label=""
image_overlay="Refresh_Off"
image_overlay="Script_Undo"
image_unselected="Toolbar_Middle_Off"
name="refresh_avatars"
name="undo_change"
tool_tip="Undo the last rotation change"
width="20"
width="18"
top_pad="0"
left="4">
<button.commit_callback
function="Poser.UndoLastRotation"/>
</button>
<button
height="21"
follows="top|left"
layout="topleft"
label=""
image_overlay="Script_Redo"
image_unselected="Toolbar_Middle_Off"
name="button_redo_change"
tool_tip="Re-do the last Undo change"
enabled="false"
width="18"
top_delta="0"
left_pad="1">
<button.commit_callback
function="Poser.RedoLastRotation"/>
</button>
<button
height="21"
follows="top|left"
layout="topleft"
label=""
image_overlay="Inv_TrashOpen"
image_unselected="Toolbar_Middle_Off"
name="refresh_avatars"
tool_tip="Double click to reset all selected body parts to when you first started posing"
width="18"
top_delta="0"
left_pad="1">
<button.commit_callback
function="Pose.PoseJointsReset"/>
</button>
<button
follows="left|top"
height="21"
@ -640,31 +671,16 @@ width="565">
tool_tip="Make smaller changes when using the trackball."
left_pad="1"
top_delta="0"
width="20" >
width="18" >
<button.commit_callback
function="Poser.ToggleTrackPadSensitivity"/>
</button>
<button
height="21"
follows="top|left"
layout="topleft"
label=""
image_overlay="Inv_TrashOpen"
image_unselected="Toolbar_Middle_Off"
name="refresh_avatars"
tool_tip="Double click to reset all selected body parts to when you first started posing"
width="20"
top_delta="0"
left_pad="1">
<button.commit_callback
function="Pose.PoseJointsReset"/>
</button>
<button
follows="left|top"
height="21"
is_toggle="true"
layout="topleft"
label="Mirror"
label="Mirr."
image_hover_unselected="Toolbar_Middle_Over"
image_selected="Toolbar_Middle_Selected"
image_unselected="Toolbar_Middle_Off"
@ -676,7 +692,7 @@ width="565">
tool_tip="Change the opposite joint, like in a mirror."
left_pad="1"
top_delta="0"
width="45" >
width="38" >
<button.commit_callback
function="Poser.ToggleMirrorChanges"/>
</button>
@ -685,7 +701,7 @@ width="565">
height="21"
is_toggle="true"
layout="topleft"
label="Symp."
label="Sym."
image_hover_unselected="Toolbar_Middle_Over"
image_selected="Toolbar_Middle_Selected"
image_unselected="Toolbar_Middle_Off"
@ -697,7 +713,7 @@ width="565">
tool_tip="Change the opposite joint, but in the same way."
left_pad="1"
top_delta="0"
width="45" >
width="38" >
<button.commit_callback
function="Poser.ToggleSympatheticChanges"/>
</button>