diff --git a/cmake/filelistGuiBase.cmake b/cmake/filelistGuiBase.cmake index 9318a582603..a1d9c7c5242 100644 --- a/cmake/filelistGuiBase.cmake +++ b/cmake/filelistGuiBase.cmake @@ -1,5 +1,11 @@ set( guibase_sources BaseApplication.cpp + KeyFrameEditor/KeyFrameEditor.cpp + KeyFrameEditor/KeyFrameEditorFrame.cpp + KeyFrameEditor/KeyFrameEditorFrameScale.cpp + KeyFrameEditor/KeyFrameEditorScrollArea.cpp + KeyFrameEditor/KeyFrameEditorTimeScale.cpp + KeyFrameEditor/KeyFrameEditorValueScale.cpp SelectionManager/SelectionManager.cpp Timeline/HelpDialog.cpp Timeline/TimelineFrameSelector.cpp @@ -30,6 +36,12 @@ set( guibase_headers BaseApplication.hpp MainWindowInterface.hpp RaGuiBase.hpp + KeyFrameEditor/KeyFrameEditor.h + KeyFrameEditor/KeyFrameEditorFrame.h + KeyFrameEditor/KeyFrameEditorFrameScale.h + KeyFrameEditor/KeyFrameEditorScrollArea.h + KeyFrameEditor/KeyFrameEditorTimeScale.h + KeyFrameEditor/KeyFrameEditorValueScale.h SelectionManager/SelectionManager.hpp Timeline/HelpDialog.hpp Timeline/Configurations.h @@ -63,6 +75,7 @@ set( guibase_inlines ) set( guibase_uis + KeyFrameEditor/KeyFrameEditor.ui Timeline/HelpDialog.ui Timeline/Timeline.ui ) diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditor.cpp b/src/GuiBase/KeyFrameEditor/KeyFrameEditor.cpp new file mode 100644 index 00000000000..6bdf0a35573 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditor.cpp @@ -0,0 +1,94 @@ +#include "ui_KeyFrameEditor.h" +#include +#include + +#include + +#include +#include + +#include +#include + +namespace Ra::GuiBase { + +KeyFrameEditor::KeyFrameEditor( Scalar maxTime, QWidget* parent ) : + QDialog( parent ), + ui( new Ui::KeyFrameEditor ) { + ui->setupUi( this ); + + // set sizePolicy to allow zoom in scrollArea + ui->scrollAreaWidgetContents->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Preferred ); + // first draw of ruler with current width (default in Timeline.ui) of dialog + ui->scrollArea->onDrawRuler( width() - 2, + height() - 2 ); // left/right border width = 2 *1 pixel + + // --- SET INTERNAL REFERENCES --- + ui->m_editorFrame->setEditorUi( ui ); + ui->m_timeScale->setScrollArea( ui->scrollArea ); + ui->m_valueScale->setScrollArea( ui->scrollArea ); + ui->m_frameScale->setScrollArea( ui->scrollArea ); + ui->m_frameScale->setEditorFrame( ui->m_editorFrame ); + + // set duration + ui->m_editorFrame->setDuration( maxTime ); + + // --- CREATE INTERNAL CONNECTIONS --- + connect( ui->m_editorFrame, &KeyFrameEditorFrame::updated, [=]() { update(); } ); + + // --- CONNECT INTERNAL SIGNALS TO EXTERNAL SIGNALS --- + connect( ui->m_editorFrame, + &KeyFrameEditorFrame::cursorChanged, + this, + &KeyFrameEditor::cursorChanged ); + connect( ui->m_editorFrame, + &KeyFrameEditorFrame::keyFrameChanged, + this, + &KeyFrameEditor::keyFrameChanged ); + connect( ui->m_editorFrame, + &KeyFrameEditorFrame::keyFrameAdded, + this, + &KeyFrameEditor::keyFrameAdded ); + connect( ui->m_editorFrame, + &KeyFrameEditorFrame::keyFrameDeleted, + this, + &KeyFrameEditor::keyFrameDeleted ); + connect( ui->m_editorFrame, + &KeyFrameEditorFrame::keyFrameMoved, + this, + &KeyFrameEditor::keyFrameMoved ); + connect( ui->m_editorFrame, + &KeyFrameEditorFrame::keyFramesMoved, + this, + &KeyFrameEditor::keyFramesMoved ); +} + +KeyFrameEditor::~KeyFrameEditor() { + delete ui; +} + +void KeyFrameEditor::onChangeCursor( Scalar time ) { + ui->m_editorFrame->onChangeCursor( time, false ); + update(); +} + +void KeyFrameEditor::onChangeDuration( Scalar duration ) { + ui->m_editorFrame->setDuration( duration ); + update(); +} + +void KeyFrameEditor::onUpdateKeyFrames( Scalar currentTime ) { + ui->m_editorFrame->onUpdateKeyFrames( currentTime ); +} + +void KeyFrameEditor::resizeEvent( QResizeEvent* event ) { + + ui->scrollArea->onDrawRuler( event->size().width() - 2, event->size().height() - 2 ); +} + +void KeyFrameEditor::setKeyFramedValue( const std::string& name, KeyFrame* frame ) { + ui->m_valueName->setText( QString::fromStdString( name ) ); + ui->m_editorFrame->setKeyFramedValue( frame ); +} + +} // namespace Ra::GuiBase diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditor.h b/src/GuiBase/KeyFrameEditor/KeyFrameEditor.h new file mode 100644 index 00000000000..d974fd04c8d --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditor.h @@ -0,0 +1,75 @@ +#ifndef RADIUMENGINE_KEYFRAME_EDITOR_H +#define RADIUMENGINE_KEYFRAME_EDITOR_H + +#include + +#include +#include + +#include +#include +#include +#include + +#include + +namespace Ra::Core::Animation { +class KeyFramedValueBase; +} + +namespace Ui { +class KeyFrameEditor; +} // namespace Ui + +namespace Ra::GuiBase { + +class RA_GUIBASE_API KeyFrameEditor : public QDialog +{ + Q_OBJECT + public: + using KeyFrame = Ra::Core::Animation::KeyFramedValueBase; + explicit KeyFrameEditor( Scalar maxTime = 0, QWidget* parent = nullptr ); + ~KeyFrameEditor() override; + + /// Set the KeyFramedValue to edit. + void setKeyFramedValue( const std::string& name, KeyFrame* frame ); + + signals: + /// Emitted when the user changes the current time in the editor frame. + void cursorChanged( Scalar time ); + + /// Emitted when the user changes a keyframe's value in the editor frame. + void keyFrameChanged( Scalar time ); + + /// Emitted when the user adds a keyframe to the KeyFramedValue in the editor frame. + void keyFrameAdded( Scalar time ); + + /// Emitted when the user removes a keyframe from the KeyFramedValue in the editor frame. + void keyFrameDeleted( Scalar time ); + + /// Emitted when the user changes a keyframe's time in the editor frame. + void keyFrameMoved( Scalar time0, Scalar time1 ); + + /// Emitted when the user offsets keyframes time in the editor frame. + void keyFramesMoved( Scalar time, Scalar offset ); + + public slots: + /// Update the editor frame to match the given time. + void onChangeCursor( Scalar time ); + + /// Update the editor frame to match the given duration. + void onChangeDuration( Scalar duration ); + + /// Update the editor frame to match the updated KeyFramedValue. + void onUpdateKeyFrames( Scalar currentTime ); + + protected: + void resizeEvent( QResizeEvent* ev ) override; + + private: + Ui::KeyFrameEditor* ui; +}; + +} // namespace Ra::GuiBase + +#endif // RADIUMENGINE_KEYFRAME_EDITOR_H diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditor.ui b/src/GuiBase/KeyFrameEditor/KeyFrameEditor.ui new file mode 100644 index 00000000000..b6dd92ca971 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditor.ui @@ -0,0 +1,1347 @@ + + + KeyFrameEditor + + + + 0 + 0 + 848 + 403 + + + + + 0 + 0 + + + + + 848 + 160 + + + + + 16777215 + 16777215 + + + + Form + + + Qt::LeftToRight + + + + QLayout::SetDefaultConstraint + + + + + 0 + + + + + NAME + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 165 + 20 + + + + + + + + + 0 + 0 + + + + + 80 + 20 + + + + + + + + + 231 + 231 + 231 + + + + + + + 85 + 85 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 202 + 202 + 202 + + + + + + + 35 + 35 + 35 + + + + + + + 184 + 184 + 184 + + + + + + + 231 + 231 + 231 + + + + + + + 255 + 0 + 0 + + + + + + + 231 + 231 + 231 + + + + + + + 85 + 85 + 255 + + + + + + + 85 + 85 + 255 + + + + + + + 118 + 118 + 118 + + + + + + + 81 + 81 + 81 + + + + + + + 0 + 0 + 0 + + + + + + + 231 + 231 + 231 + + + + + + + + + 231 + 231 + 231 + + + + + + + 85 + 85 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 202 + 202 + 202 + + + + + + + 35 + 35 + 35 + + + + + + + 184 + 184 + 184 + + + + + + + 231 + 231 + 231 + + + + + + + 255 + 0 + 0 + + + + + + + 231 + 231 + 231 + + + + + + + 85 + 85 + 255 + + + + + + + 85 + 85 + 255 + + + + + + + 118 + 118 + 118 + + + + + + + 81 + 81 + 81 + + + + + + + 0 + 0 + 0 + + + + + + + 231 + 231 + 231 + + + + + + + + + 35 + 35 + 35 + + + + + + + 85 + 85 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 202 + 202 + 202 + + + + + + + 35 + 35 + 35 + + + + + + + 184 + 184 + 184 + + + + + + + 35 + 35 + 35 + + + + + + + 255 + 0 + 0 + + + + + + + 35 + 35 + 35 + + + + + + + 85 + 85 + 255 + + + + + + + 85 + 85 + 255 + + + + + + + 118 + 118 + 118 + + + + + + + 81 + 81 + 81 + + + + + + + 0 + 0 + 0 + + + + + + + 231 + 231 + 231 + + + + + + + + <html><head/><body><p><span style=" font-size:10pt; font-weight:600;">Postition of cursor</span></p></body></html> + + + background-color: #5555ff + + + Qt::AlignCenter + + + QAbstractSpinBox::NoButtons + + + 3 + + + 999.000000000000000 + + + 1.000000000000000 + + + 0.500000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + 20 + 20 + + + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + <html><head/><body><p><span style=" font-size:10pt; font-weight:600; color:#ffffff;">Insert/replace KeyFrame on cursor</span></p></body></html> + + + background-color:black; + + + ... + + + + :/images/key.png:/images/key.png + + + + 15 + 15 + + + + + + + + + 20 + 20 + + + + + 16777215 + 20 + + + + Numbers of KeyFrames + + + 0 + + + Qt::AlignCenter + + + + + + + + 20 + 20 + + + + + + + + + 128 + 128 + 128 + + + + + + + 128 + 128 + 128 + + + + + + + 128 + 128 + 128 + + + + + + + + + 128 + 128 + 128 + + + + + + + 128 + 128 + 128 + + + + + + + 128 + 128 + 128 + + + + + + + + + 128 + 128 + 128 + + + + + + + 128 + 128 + 128 + + + + + + + 128 + 128 + 128 + + + + + + + + <html><head/><body><p><span style=" font-size:10pt; font-weight:600;">Delete KeyFrame on cursor</span></p></body></html> + + + ... + + + + :/images/deleteKey.png:/images/deleteKey.png + + + + 15 + 15 + + + + + + + + + + 0 + + + + + 0 + + + 0 + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: red + + + Pos X + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: green + + + Pos Y + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: blue + + + Pos Z + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: orange + + + Pos W + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: red + + + Rot X + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: green + + + Rot Y + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: blue + + + Rot Z + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: red + + + Scale X + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: green + + + Scale Y + + + + + + + + 75 + 20 + + + + + 75 + 20 + + + + background: darkGray; color: blue + + + Scale Z + + + + + + + background: darkGray + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + 0 + + + + + + 61 + 20 + + + + + 51 + 20 + + + + background: darkgray + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + 0 + 20 + + + + + 16777215 + 20 + + + + background: darkgray + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + background: darkgray + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + background: darkgray + + + QFrame::StyledPanel + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 679 + 305 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 61 + 20 + + + + + 61 + 20 + + + + background: darkgray + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + 0 + 0 + + + + + 0 + 20 + + + + + 16777215 + 20 + + + + background: darkgray + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + + + + + Ra::GuiBase::KeyFrameEditorFrame + QFrame +
GuiBase/KeyFrameEditor/KeyFrameEditorFrame.h
+ 1 +
+ + Ra::GuiBase::KeyFrameEditorScrollArea + QScrollArea +
GuiBase/KeyFrameEditor/KeyFrameEditorScrollArea.h
+ 1 +
+ + Ra::GuiBase::KeyFrameEditorTimeScale + QFrame +
GuiBase/KeyFrameEditor/KeyFrameEditorTimeScale.h
+ 1 +
+ + Ra::GuiBase::KeyframeEditorValueScale + QFrame +
GuiBase/KeyFrameEditor/KeyFrameEditorValueScale.h
+ 1 +
+ + Ra::GuiBase::KeyFrameEditorFrameScale + QFrame +
GuiBase/KeyFrameEditor/KeyFrameEditorFrameScale.h
+ 1 +
+
+ + + + +
diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrame.cpp b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrame.cpp new file mode 100644 index 00000000000..ddefc951ba2 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrame.cpp @@ -0,0 +1,1082 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +static constexpr int CTRL_PT_RAD = 5; + +using namespace Ra::Core::Utils; + +template +using KeyFramedValue = Ra::Core::Animation::KeyFramedValue; + +namespace Ra::GuiBase { + +KeyFrameEditorFrame::KeyFrameEditorFrame( QWidget* parent ) : QFrame( parent ) { + m_painter = new QPainter( this ); +} + +KeyFrameEditorFrame::~KeyFrameEditorFrame() { + delete m_painter; +} + +void KeyFrameEditorFrame::setEditorUi( Ui::KeyFrameEditor* ui ) { + m_ui = ui; + if ( m_ui == nullptr ) { return; } + + m_ui->m_currentTime_dsb->setMaximum( double( m_ui->scrollArea->getMaxTime() ) ); + + // create connections + connect( m_ui->m_currentTime_dsb, + static_cast( &QDoubleSpinBox::valueChanged ), + [=]( double current ) { onChangeCursor( Scalar( current ) ); } ); + connect( ui->m_addKeyframe, &QToolButton::clicked, this, &KeyFrameEditorFrame::onAddKeyFrame ); + connect( + ui->m_deleteKeyframe, &QToolButton::clicked, this, &KeyFrameEditorFrame::onDeleteKeyFrame ); + + connect( ui->scrollArea, + &KeyFrameEditorScrollArea::addKeyFrame, + this, + &KeyFrameEditorFrame::onAddKeyFrame ); + connect( ui->scrollArea, + &KeyFrameEditorScrollArea::removeKeyFrame, + this, + &KeyFrameEditorFrame::onDeleteKeyFrame ); + connect( ui->scrollArea, + &KeyFrameEditorScrollArea::nextKeyFrame, + this, + &KeyFrameEditorFrame::onSetCursorToNextKeyFrame ); + connect( ui->scrollArea, + &KeyFrameEditorScrollArea::previousKeyFrame, + this, + &KeyFrameEditorFrame::onSetCursorToPreviousKeyFrame ); + + connect( ui->scrollArea, &KeyFrameEditorScrollArea::stepChanged, [this]( Scalar step ) { + m_ui->m_currentTime_dsb->setSingleStep( 0.5 * double( step ) ); + } ); + + m_displayCurve[0] = m_ui->m_posX->isChecked(); + connect( m_ui->m_posX, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[0] = on; + update(); + } ); + m_displayCurve[1] = m_ui->m_posY->isChecked(); + connect( m_ui->m_posY, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[1] = on; + update(); + } ); + m_displayCurve[2] = m_ui->m_posZ->isChecked(); + connect( m_ui->m_posZ, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[2] = on; + update(); + } ); + m_displayCurve[3] = m_ui->m_posW->isChecked(); + connect( m_ui->m_posW, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[3] = on; + update(); + } ); + m_displayCurve[4] = m_ui->m_rotX->isChecked(); + connect( m_ui->m_rotX, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[4] = on; + update(); + } ); + m_displayCurve[5] = m_ui->m_rotY->isChecked(); + connect( m_ui->m_rotY, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[5] = on; + update(); + } ); + m_displayCurve[6] = m_ui->m_rotZ->isChecked(); + connect( m_ui->m_rotZ, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[6] = on; + update(); + } ); + m_displayCurve[7] = m_ui->m_scaleX->isChecked(); + connect( m_ui->m_scaleX, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[7] = on; + update(); + } ); + m_displayCurve[8] = m_ui->m_scaleY->isChecked(); + connect( m_ui->m_scaleY, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[8] = on; + update(); + } ); + m_displayCurve[9] = m_ui->m_scaleZ->isChecked(); + connect( m_ui->m_scaleZ, &QCheckBox::toggled, [=]( bool on ) { + m_displayCurve[9] = on; + update(); + } ); +} + +void KeyFrameEditorFrame::setDuration( Scalar time ) { + Scalar newDuration = std::max( time, 0_ra ); + + bool change = qAbs( newDuration - m_ui->scrollArea->getMaxTime() ) > 1e-5_ra; + + if ( change ) + { + m_ui->scrollArea->setMaxTime( newDuration ); + m_ui->scrollArea->onDrawRuler( m_ui->scrollArea->widget()->minimumWidth(), + m_ui->scrollArea->widget()->minimumHeight() ); + } + m_ui->m_currentTime_dsb->setMaximum( double( time ) ); +} + +void KeyFrameEditorFrame::setKeyFramedValue( KeyFrame* frame ) { + m_value = frame; + registerKeyFrames( true ); + update(); +} + +std::set KeyFrameEditorFrame::getKeyFrames() const { + return m_value->getTimeSchedule(); +} + +void KeyFrameEditorFrame::onChangeCursor( Scalar time, bool internal ) { + Scalar newCursor = std::max( 0_ra, time ); + + if ( internal ) { newCursor = nearestStep( newCursor ); } + + bool change = std::abs( newCursor - m_cursor ) > 1e-5_ra; + + if ( change ) + { + m_cursor = newCursor; + if ( internal ) { emit cursorChanged( m_cursor ); } + update(); + } + updateCursorSpin(); +} + +void KeyFrameEditorFrame::onUpdateKeyFrames( Scalar currentTime ) { + m_cursor = currentTime; + + updateCursorSpin(); + m_ui->m_nbKeyFrame->setText( QString::number( m_value->getTimeSchedule().size() ) ); + + registerKeyFrames(); + update(); +} + +void KeyFrameEditorFrame::onAddKeyFrame() { + auto times = m_value->getTimeSchedule(); + auto it = times.find( m_cursor ); + if ( it != times.end() ) { return; } + + emit keyFrameAdded( m_cursor ); + + onUpdateKeyFrames( m_cursor ); +} + +void KeyFrameEditorFrame::onDeleteKeyFrame() { + auto times = m_value->getTimeSchedule(); + auto it = times.find( m_cursor ); + if ( it == times.end() ) { return; } + + emit keyFrameDeleted( m_cursor ); + + onUpdateKeyFrames( m_cursor ); +} + +void KeyFrameEditorFrame::onMoveKeyFrame( Scalar time0, Scalar time1 ) { + if ( Ra::Core::Math::areApproxEqual( time0, time1 ) ) { return; } + m_cursor = time1; + + emit keyFrameMoved( time0, time1 ); + + onUpdateKeyFrames( m_cursor ); +} + +void KeyFrameEditorFrame::onMoveKeyFrames( Scalar time, Scalar offset ) { + if ( Ra::Core::Math::areApproxEqual( offset, 0_ra ) ) { return; } + + auto times = m_value->getTimeSchedule(); + auto it = times.find( time ); + if ( it == times.end() ) { return; } + + Scalar left = ( offset > 0 ) ? ( time ) : ( time + offset ); + + emit keyFramesMoved( time, offset ); + + if ( m_cursor >= left ) + { + m_cursor = std::max( m_cursor + offset, 0_ra ); + updateCursorSpin(); + emit cursorChanged( m_cursor ); + } + onUpdateKeyFrames( m_cursor ); +} + +void KeyFrameEditorFrame::deleteZone( Scalar time, Scalar time2 ) { + Scalar left = qMin( time, time2 ); + Scalar right = qMax( time, time2 ); + + Scalar dist = right - left; + + auto times = m_value->getTimeSchedule(); + auto it = times.begin(); + bool first = true; + while ( it != times.end() ) + { + Scalar keyFrame = *it; + + if ( keyFrame >= left ) + { + if ( keyFrame > right ) + { + if ( first ) + { + emit keyFramesMoved( keyFrame, -dist ); + first = false; + } + } + else + { emit keyFrameDeleted( keyFrame ); } + } + ++it; + } + + emit cursorChanged( m_cursor ); + + onUpdateKeyFrames( m_cursor ); +} + +void KeyFrameEditorFrame::onSetCursorToPreviousKeyFrame() { + auto times = m_value->getTimeSchedule(); + auto it = times.rbegin(); + while ( it != times.rend() && *it >= m_cursor ) + it++; + + if ( it != times.rend() ) { onChangeCursor( *it ); } +} + +void KeyFrameEditorFrame::onSetCursorToNextKeyFrame() { + auto times = m_value->getTimeSchedule(); + auto it = times.begin(); + while ( it != times.end() && *it <= m_cursor ) + it++; + + if ( it != times.end() ) { onChangeCursor( *it ); } +} + +#define drawVector( VECTOR_TYPE, N ) \ + [&path, fromValueToPixel, this]( int x, const VECTOR_TYPE& v ) { \ + int y; \ + for ( uint i = 0; i < N; ++i ) \ + { \ + if ( m_displayCurve[i] ) \ + { \ + fromValueToPixel( v( i ), y ); \ + path[i].lineTo( QPoint( x, y ) ); \ + } \ + } \ + } + +#define drawSampled( drawFunc ) \ + { \ + int p; \ + Scalar t0 = *times.begin(); \ + for ( auto t1 : times ) \ + { \ + fromTimeToPixel( t1, p ); \ + if ( p < sliderH ) \ + { \ + std::exchange( t0, t1 ); \ + continue; \ + } \ + fromTimeToPixel( t0, p ); \ + if ( p > sliderH + areaWidth ) \ + { \ + std::exchange( t0, t1 ); \ + continue; \ + } \ + const int N = int( ( t1 - t0 ) / stepT ); \ + for ( Scalar t = t0; t <= t1; t += 1_ra / N ) \ + { \ + fromTimeToPixel( t, p ); \ + drawFunc( p, kf->at( t ) ); \ + } \ + std::exchange( t0, t1 ); \ + } \ + } + +void KeyFrameEditorFrame::paintEvent( QPaintEvent* ) { + int h = height(); + int w = width(); + int nT = m_ui->scrollArea->getNbIntervalTime(); + Scalar stepT = m_ui->scrollArea->getStepTime(); + Scalar zeroTime = m_ui->scrollArea->getZeroTime(); + Scalar pixPerSec = m_ui->scrollArea->getPixPerSec(); + int nV = m_ui->scrollArea->getNbIntervalValue(); + Scalar stepV = m_ui->scrollArea->getStepValue(); + Scalar zeroValue = m_ui->scrollArea->getZeroValue(); + Scalar pixPerVal = m_ui->scrollArea->getPixPerValue(); + + m_painter->begin( this ); + + // DRAW GRID + m_painter->setPen( Qt::darkGray ); + for ( int i = 1; i < nT; i++ ) + { + int x = static_cast( i * stepT * pixPerSec ); + m_painter->drawLine( x, 0, x, h ); + } + m_painter->setPen( Qt::gray ); + for ( int i = 1; i < nT; i++ ) + { + int middleX = int( ( i + 0.5_ra ) * stepT * pixPerSec ); + m_painter->drawLine( middleX, 0, middleX, h ); + } + m_painter->setPen( Qt::darkGray ); + for ( int i = 1; i < nV; i++ ) + { + int y = int( i * stepV * pixPerVal ); + m_painter->drawLine( int( zeroTime ), y, w, y ); + } + m_painter->setPen( Qt::gray ); + for ( int i = 1; i < nV; i++ ) + { + int middleY = int( ( i + 0.5_ra ) * stepV * pixPerVal ); + m_painter->drawLine( int( zeroTime ), middleY, w, middleY ); + } + m_painter->setPen( QPen( Qt::black, 3 ) ); + m_painter->drawLine( int( zeroTime ), 0, int( zeroTime ), h ); + m_painter->drawLine( + 0, int( nV / 2 * stepV * pixPerVal ), w, int( nV / 2 * stepV * pixPerVal ) ); + + // DRAW CURSOR + m_painter->setPen( QPen( Qt::blue, 3 ) ); + int xCursor = static_cast( zeroTime + m_cursor * pixPerSec ); + m_painter->drawLine( xCursor, 0, xCursor, h ); + + // DRAW CURVES + if ( m_curveControlPoints.size() ) + { + m_painter->setPen( Qt::black ); + m_painter->setBrush( QColor( 255, 127, 0, 255 ) ); + static QColor curveColor[10] = {Qt::red, + Qt::green, + Qt::blue, + Qt::darkYellow, + Qt::red, + Qt::green, + Qt::blue, + Qt::red, + Qt::green, + Qt::blue}; + const auto fromValueToPixel = [=]( Scalar value, int& y ) { + y = int( zeroValue - value * pixPerVal ); + }; + + const auto fromTimeToPixel = [=]( Scalar time, int& x ) { + x = int( zeroTime + time * pixPerSec ); + }; + + const auto fromTimeValueToPixels = [=]( Scalar time, Scalar value, int& x, int& y ) { + fromTimeToPixel( time, x ); + fromValueToPixel( value, y ); + }; + + // fill strokes + QPainterPath path[10] = {QPainterPath(), + QPainterPath(), + QPainterPath(), + QPainterPath(), + QPainterPath(), + QPainterPath(), + QPainterPath(), + QPainterPath(), + QPainterPath(), + QPainterPath()}; + + // CURVES + const auto drawFlat = + [fromTimeValueToPixels]( const CurveControlPoints& curve, int y0, QPainterPath& path ) { + int x, y; + // flat between control points + for ( const auto& P : curve ) + { + fromTimeValueToPixels( P.x(), P.y(), x, y ); + path.lineTo( QPoint( x, y0 ) ); + path.lineTo( QPoint( x, y ) ); + y0 = y; + } + }; + + const auto drawQuaternion = + [&path, fromValueToPixel, this]( int x, const Ra::Core::Quaternion& Q ) { + int y; + auto angles = Q.matrix().eulerAngles( 0, 1, 2 ); + if ( m_displayCurve[4] ) + { + fromValueToPixel( angles.x(), y ); + path[4].lineTo( QPoint( x, y ) ); + } + if ( m_displayCurve[5] ) + { + fromValueToPixel( angles.y(), y ); + path[5].lineTo( QPoint( x, y ) ); + } + if ( m_displayCurve[6] ) + { + fromValueToPixel( angles.z(), y ); + path[6].lineTo( QPoint( x, y ) ); + } + }; + + // first: flat before first sample + int px, py; + for ( size_t channel = 0; channel < 10; ++channel ) + { + if ( !m_displayCurve[channel] ) continue; + const auto& curve = m_curveControlPoints[channel]; + const auto& P = curve[0]; + fromTimeValueToPixels( P.x(), P.y(), px, py ); + path[channel].moveTo( QPoint( int( zeroTime ), py ) ); + path[channel].lineTo( QPoint( px, py ) ); + } + + // then: sample between times + const int sliderH = m_ui->scrollArea->horizontalScrollBar()->value(); + const int areaWidth = m_ui->scrollArea->width(); + + const auto times = m_value->getTimeSchedule(); + if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] ) { drawFlat( m_curveControlPoints[0], py, path[0] ); } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] ) { drawFlat( m_curveControlPoints[0], py, path[0] ); } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] ) + { + const auto drawScalar = [&path, fromValueToPixel]( int x, Scalar v ) { + int y; + fromValueToPixel( v, y ); + path[0].lineTo( QPoint( x, y ) ); + }; + drawSampled( drawScalar ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] || m_displayCurve[1] ) + { drawSampled( drawVector( Ra::Core::Vector2, 2 ) ); } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] || m_displayCurve[1] || m_displayCurve[2] ) + { drawSampled( drawVector( Ra::Core::Vector3, 3 ) ); } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] || m_displayCurve[1] || m_displayCurve[2] || m_displayCurve[3] ) + { drawSampled( drawVector( Ra::Core::Vector4, 4 ) ); } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] || m_displayCurve[1] || m_displayCurve[2] || m_displayCurve[3] ) + { drawSampled( drawVector( Ra::Core::Utils::Color, 4 ) ); } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] || m_displayCurve[1] || m_displayCurve[2] || m_displayCurve[3] ) + { drawSampled( drawQuaternion ); } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + if ( m_displayCurve[0] || m_displayCurve[1] || m_displayCurve[2] || m_displayCurve[3] || + m_displayCurve[4] || m_displayCurve[5] || m_displayCurve[6] || m_displayCurve[7] || + m_displayCurve[8] || m_displayCurve[9] ) + { + const auto drawTransform = [&path, fromValueToPixel, drawQuaternion, this]( + int x, const Ra::Core::Transform& T ) { + int y; + Ra::Core::Matrix3 R, S; + T.computeRotationScaling( &R, &S ); + auto drawPos = drawVector( Ra::Core::Vector3, 3 ); + drawPos( x, T.translation() ); + drawQuaternion( x, Ra::Core::Quaternion( R ) ); + if ( m_displayCurve[7] ) + { + fromValueToPixel( S( 0, 0 ), y ); + path[7].lineTo( QPoint( x, y ) ); + } + if ( m_displayCurve[8] ) + { + fromValueToPixel( S( 1, 1 ), y ); + path[8].lineTo( QPoint( x, y ) ); + } + if ( m_displayCurve[9] ) + { + fromValueToPixel( S( 2, 2 ), y ); + path[9].lineTo( QPoint( x, y ) ); + } + }; + drawSampled( drawTransform ); + } + } + + // finally: flat after last + for ( size_t channel = 0; channel < 10; ++channel ) + { + if ( !m_displayCurve[channel] ) continue; + const auto& curve = m_curveControlPoints[channel]; + const auto& P = curve.back(); + fromTimeValueToPixels( P.x(), P.y(), px, py ); + path[channel].lineTo( QPoint( px, py ) ); + path[channel].lineTo( QPoint( w, py ) ); + } + // paint curves + for ( size_t channel = 0; channel < 10; ++channel ) + { + if ( !m_displayCurve[channel] ) continue; + m_painter->strokePath( path[channel], QPen( curveColor[channel], 2 ) ); + } + + // CONTROL POINTS + for ( size_t channel = 0; channel < 10; ++channel ) + { + if ( !m_displayCurve[channel] ) { continue; } + auto curve = m_curveControlPoints[channel]; + for ( const auto& P : curve ) + { + fromTimeValueToPixels( P.x(), P.y(), px, py ); + m_painter->drawEllipse( QPoint( px, py ), CTRL_PT_RAD, CTRL_PT_RAD ); + } + } + + // REDRAW CURRENT CONTROL POINT + m_painter->setBrush( QColor( 255, 255, 0, 255 ) ); + int i = m_currentControlPoint( 0 ); + int j = m_currentControlPoint( 1 ); + if ( i != -1 && j != -1 ) + { + const auto& P = m_curveControlPoints[uint( i )][uint( j )]; + fromTimeValueToPixels( P.x(), P.y(), px, py ); + m_painter->drawEllipse( QPoint( px, py ), CTRL_PT_RAD, CTRL_PT_RAD ); + } + } + + m_painter->end(); + + emit updated(); +} +#undef drawSampled +#undef drawVector + +void KeyFrameEditorFrame::mousePressEvent( QMouseEvent* event ) { + Scalar zeroTime = m_ui->scrollArea->getZeroTime(); + Scalar pixPerSec = m_ui->scrollArea->getPixPerSec(); + + Scalar newFrame = std::max( ( event->x() - zeroTime ) / pixPerSec, 0.0_ra ); + bool shiftDown = event->modifiers() & Qt::Modifier::SHIFT; + bool ctrlDown = event->modifiers() & Qt::Modifier::CTRL; + // ---------------------- LEFT CLICK -------------------------------------- + if ( event->button() == Qt::LeftButton ) + { + // check if grabbing a control point + Scalar zeroValue = m_ui->scrollArea->getZeroValue(); + Scalar pixPerVal = m_ui->scrollArea->getPixPerValue(); + for ( size_t i = 0; i < m_curveControlPoints.size(); ++i ) + { + if ( !m_displayCurve[i] ) { continue; } + const auto& curve = m_curveControlPoints[i]; + for ( size_t j = 0; j < curve.size(); ++j ) + { + const auto& P = curve[j]; + Scalar x = zeroTime + P.x() * pixPerSec - event->x(); + Scalar y = zeroValue - P.y() * pixPerVal - event->y(); + if ( x * x + y * y < CTRL_PT_RAD * CTRL_PT_RAD ) + { + m_currentControlPoint = {i, j}; + m_mouseLeftClicked = true; + event->accept(); + return; + } + } + } + // move cursor without render + if ( ctrlDown ) { onChangeCursor( newFrame, false ); } + // delete KeyFrames between cursor and newCursor + else if ( shiftDown ) + { deleteZone( m_cursor, newFrame ); } + // move cursor and update renderer + else + { + onChangeCursor( newFrame ); + m_mouseLeftClicked = true; + } + } + // ------------------ RIGHT CLICK ------------------------------------- + else if ( event->button() == Qt::RightButton ) + { + auto times = m_value->getTimeSchedule(); + auto it = times.find( m_cursor ); + // if already on KeyFrame, move current KeyFrame + // ------------------- CURSOR ON KEYFRAME ----------------------- + if ( it != times.end() ) + { + Scalar nearest = nearestStep( newFrame ); + // -------------- SINGLE MOVE ------------------------------------- + if ( shiftDown ) + { + // if no KeyFrame under mouse, move KeyFrame to newFrame + if ( times.find( nearest ) == times.end() && + ( std::abs( m_cursor - nearest ) > 1e-5_ra ) ) + { onMoveKeyFrame( m_cursor, nearest ); } + } + // ---------- MULTIPLE MOVE ----------------------------------- + else + { + auto itLeft = it; + --itLeft; + Scalar left = ( it == times.begin() ) ? ( 0 ) : ( *itLeft ); + // if not before preceding KeyFrame, remove or insert time + if ( nearest > left ) { onMoveKeyFrames( m_cursor, nearest - m_cursor ); } + } + } + // ---------------- CURSOR NOT ON KEYFRAME -------------------------- + else + { + // if shiftdown, slide first right KeyFrame to the left + // --------------- MOVE RIGHT KEYFRAME TO THE LEFT ----------------- + if ( shiftDown ) + { + auto itRight = times.lower_bound( newFrame ); + // if KeyFrames on the right, remove or insert time + if ( itRight != times.end() ) { onMoveKeyFrames( *itRight, newFrame - *itRight ); } + } + // if not shiftdown, slide first left KeyFrame to the right + // ---------------- MOVE LEFT KEYFRAME TO THE RIGHT ----------- + else + { + auto itLeft = --times.lower_bound( newFrame ); + // if KeyFrames on the left, remove or insert time + if ( itLeft != times.end() ) { onMoveKeyFrames( *itLeft, newFrame - *itLeft ); } + } + } + } + // no catch mouse event + else + { + event->ignore(); + return; + } + event->accept(); +} + +#define insertVector( VECTOR_TYPE, N ) \ + VECTOR_TYPE v; \ + for ( uint i = 0; i < N; ++i ) \ + { \ + v( i ) = m_curveControlPoints[i][uint( j )].y(); \ + } \ + kf->insertKeyFrame( time, v ) + +#define fetchMatrix() \ + Ra::Core::Matrix3 { \ + Ra::Core::AngleAxis( m_curveControlPoints[4][uint( j )].y(), \ + Ra::Core::Vector3::UnitX() ) * \ + Ra::Core::AngleAxis( m_curveControlPoints[5][uint( j )].y(), \ + Ra::Core::Vector3::UnitY() ) * \ + Ra::Core::AngleAxis( m_curveControlPoints[6][uint( j )].y(), \ + Ra::Core::Vector3::UnitZ() ) \ + } + +void KeyFrameEditorFrame::mouseMoveEvent( QMouseEvent* event ) { + if ( m_mouseLeftClicked ) + { + int i = m_currentControlPoint.x(); + int j = m_currentControlPoint.y(); + if ( i != -1 && j != -1 ) + { + Scalar value = ( -event->y() + m_ui->scrollArea->getZeroValue() ) / + m_ui->scrollArea->getPixPerValue(); + Scalar time = m_curveControlPoints[uint( i )][uint( j )].x(); + if ( auto kf = dynamic_cast*>( m_value ) ) + { + kf->insertKeyFrame( time, int( std::max( value, 0_ra ) ) ); + m_curveControlPoints[uint( i )][uint( j )].y() = kf->at( time ); + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + kf->insertKeyFrame( time, int( value ) ); + m_curveControlPoints[uint( i )][uint( j )].y() = kf->at( time ); + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + kf->insertKeyFrame( time, value ); + m_curveControlPoints[uint( i )][uint( j )].y() = kf->at( time ); + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + insertVector( Ra::Core::Vector2, 2 ); + m_curveControlPoints[uint( i )][uint( j )].y() = value; + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + insertVector( Ra::Core::Vector3, 3 ); + m_curveControlPoints[uint( i )][uint( j )].y() = value; + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + insertVector( Ra::Core::Vector4, 4 ); + m_curveControlPoints[uint( i )][uint( j )].y() = value; + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + insertVector( Ra::Core::Utils::Color, 4 ); + m_curveControlPoints[uint( i )][uint( j )].y() = value; + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + Ra::Core::Matrix3 R = fetchMatrix(); + kf->insertKeyFrame( time, Ra::Core::Quaternion( R ) ); + m_curveControlPoints[uint( i )][uint( j )].y() = value; + } + if ( auto kf = dynamic_cast*>( m_value ) ) + { + Ra::Core::Vector3 P( m_curveControlPoints[0][uint( j )].y(), + m_curveControlPoints[1][uint( j )].y(), + m_curveControlPoints[2][uint( j )].y() ); + Ra::Core::Matrix3 R = fetchMatrix(); + Ra::Core::Vector3 S( m_curveControlPoints[7][uint( j )].y(), + m_curveControlPoints[8][uint( j )].y(), + m_curveControlPoints[9][uint( j )].y() ); + Ra::Core::Transform T; + T.fromPositionOrientationScale( P, R, S ); + kf->insertKeyFrame( time, T ); + m_curveControlPoints[uint( i )][uint( j )].y() = value; + } + update(); + emit keyFrameChanged( time ); + } + else + { + Scalar newCursor = std::max( ( event->x() - m_ui->scrollArea->getZeroTime() ) / + m_ui->scrollArea->getPixPerSec(), + 0_ra ); + + onChangeCursor( newCursor ); + } + } + else + { event->ignore(); } +} +#undef insertVector +#undef fetchMatrix + +void KeyFrameEditorFrame::mouseReleaseEvent( QMouseEvent* event ) { + if ( event->button() == Qt::LeftButton ) + { + m_mouseLeftClicked = false; + m_currentControlPoint = {-1, -1}; + event->accept(); + } + else + { event->ignore(); } +} + +Scalar KeyFrameEditorFrame::nearestStep( Scalar time ) const { + Scalar deltaT = TIMELINE_AUTO_SUGGEST_CURSOR_RADIUS / m_ui->scrollArea->getPixPerSec(); + + Scalar minDist = m_ui->scrollArea->getMaxTime(); + Scalar dist; + + Scalar newCursor = time; + + auto times = m_value->getTimeSchedule(); + for ( auto keyFrame : times ) + { + dist = qAbs( keyFrame - time ); + if ( dist < deltaT && dist < minDist ) + { + minDist = dist; + newCursor = keyFrame; + } + if ( time < keyFrame ) break; + } + + const auto stepTime = m_ui->scrollArea->getStepTime(); + for ( int i = 0; i < m_ui->scrollArea->getNbIntervalTime() - 1; ++i ) + { + Scalar pos = i * stepTime; + dist = qAbs( pos - time ); + if ( dist < deltaT && dist < minDist ) + { + minDist = dist; + newCursor = pos; + } + + pos = i * stepTime + 0.5_ra * stepTime; + dist = qAbs( pos - time ); + if ( dist < deltaT && dist < minDist ) + { + minDist = dist; + newCursor = pos; + } + + if ( time < pos ) break; + } + + return newCursor; +} + +#define registerValue() \ + m_curveControlPoints[0].reserve( times.size() ); \ + for ( auto t : times ) \ + { \ + m_curveControlPoints[0].push_back( {t, kf->at( t )} ); \ + } + +#define registerVector( N ) \ + for ( size_t i = 0; i < N; ++i ) \ + { \ + m_curveControlPoints[i].reserve( times.size() ); \ + } \ + for ( auto t : times ) \ + { \ + const auto f = kf->at( t ); \ + for ( uint i = 0; i < N; ++i ) \ + m_curveControlPoints[i].push_back( {t, f( i )} ); \ + } + +void KeyFrameEditorFrame::registerKeyFrames( bool newValue ) { + std::array displayCurve = m_displayCurve; + + const auto display = [=]( QCheckBox* box, bool on ) { + box->setChecked( on ); + on ? box->show() : box->hide(); + }; + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, false ); + display( m_ui->m_posY, false ); + display( m_ui->m_posZ, false ); + display( m_ui->m_posW, false ); + display( m_ui->m_scaleX, false ); + display( m_ui->m_scaleY, false ); + display( m_ui->m_scaleZ, false ); + display( m_ui->m_rotX, false ); + display( m_ui->m_rotY, false ); + display( m_ui->m_rotZ, false ); + } + + if ( m_value == nullptr ) { return; } + // for each keyframe channel, collect the curve control points + for ( auto& curve : m_curveControlPoints ) + { + curve.clear(); + } + + auto times = m_value->getTimeSchedule(); + m_ui->m_nbKeyFrame->setText( QString::number( times.size() ) ); + if ( auto kf = dynamic_cast*>( m_value ) ) + { + m_curveControlPoints[0].reserve( times.size() ); + for ( auto t : times ) + { + m_curveControlPoints[0].push_back( {t, ( kf->at( t ) ? 1 : 0 )} ); + } + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + m_ui->m_posX->setText( "x" ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + registerValue(); + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + m_ui->m_posX->setText( "x" ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + registerValue(); + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + m_ui->m_posX->setText( "x" ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + registerVector( 2 ); + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + display( m_ui->m_posY, true ); + m_ui->m_posX->setText( "x" ); + m_ui->m_posY->setText( "y" ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + registerVector( 3 ); + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + display( m_ui->m_posY, true ); + display( m_ui->m_posZ, true ); + m_ui->m_posX->setText( "x" ); + m_ui->m_posY->setText( "y" ); + m_ui->m_posZ->setText( "z" ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + registerVector( 4 ); + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + display( m_ui->m_posY, true ); + display( m_ui->m_posZ, true ); + display( m_ui->m_posW, true ); + m_ui->m_posX->setText( "x" ); + m_ui->m_posY->setText( "y" ); + m_ui->m_posZ->setText( "z" ); + m_ui->m_posW->setText( "w" ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + registerVector( 4 ); + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + display( m_ui->m_posY, true ); + display( m_ui->m_posZ, true ); + display( m_ui->m_posW, true ); + m_ui->m_posX->setText( "r" ); + m_ui->m_posY->setText( "g" ); + m_ui->m_posZ->setText( "b" ); + m_ui->m_posW->setText( "a" ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + m_curveControlPoints[4].reserve( times.size() ); + m_curveControlPoints[5].reserve( times.size() ); + m_curveControlPoints[6].reserve( times.size() ); + for ( auto t : times ) + { + const auto f = kf->at( t ).matrix().eulerAngles( 0, 1, 2 ); + m_curveControlPoints[4].push_back( {t, f.x()} ); + m_curveControlPoints[5].push_back( {t, f.y()} ); + m_curveControlPoints[6].push_back( {t, f.z()} ); + } + + if ( m_ui != nullptr ) + { + display( m_ui->m_rotX, true ); + display( m_ui->m_rotY, true ); + display( m_ui->m_rotZ, true ); + } + } + else if ( auto kf = dynamic_cast*>( m_value ) ) + { + for ( size_t i = 0; i < 10; ++i ) + { + m_curveControlPoints[i].reserve( times.size() ); + } + for ( auto t : times ) + { + const auto f = kf->at( t ); + const auto T = f.translation(); + Ra::Core::Matrix3 R, S; + f.computeRotationScaling( &R, &S ); + auto angles = R.eulerAngles( 0, 1, 2 ); + m_curveControlPoints[0].push_back( {t, T( 0 )} ); + m_curveControlPoints[1].push_back( {t, T( 1 )} ); + m_curveControlPoints[2].push_back( {t, T( 2 )} ); + m_curveControlPoints[4].push_back( {t, angles.x()} ); + m_curveControlPoints[5].push_back( {t, angles.y()} ); + m_curveControlPoints[6].push_back( {t, angles.z()} ); + m_curveControlPoints[7].push_back( {t, S( 0, 0 )} ); + m_curveControlPoints[8].push_back( {t, S( 1, 1 )} ); + m_curveControlPoints[9].push_back( {t, S( 2, 2 )} ); + } + + if ( m_ui != nullptr ) + { + display( m_ui->m_posX, true ); + display( m_ui->m_posY, true ); + display( m_ui->m_posZ, true ); + display( m_ui->m_rotX, true ); + display( m_ui->m_rotY, true ); + display( m_ui->m_rotZ, true ); + display( m_ui->m_scaleX, true ); + display( m_ui->m_scaleY, true ); + display( m_ui->m_scaleZ, true ); + m_ui->m_posX->setText( "x" ); + m_ui->m_posY->setText( "y" ); + m_ui->m_posZ->setText( "z" ); + } + } + else + { LOG( logWARNING ) << "[KeyFrameEditor] Unknown KeyFrame type, cannot edit."; } + + if ( !newValue ) + { + m_displayCurve = displayCurve; + m_ui->m_posX->setChecked( m_displayCurve[0] ); + m_ui->m_posY->setChecked( m_displayCurve[1] ); + m_ui->m_posZ->setChecked( m_displayCurve[2] ); + m_ui->m_posW->setChecked( m_displayCurve[3] ); + m_ui->m_rotX->setChecked( m_displayCurve[4] ); + m_ui->m_rotY->setChecked( m_displayCurve[5] ); + m_ui->m_rotZ->setChecked( m_displayCurve[6] ); + m_ui->m_scaleX->setChecked( m_displayCurve[7] ); + m_ui->m_scaleY->setChecked( m_displayCurve[8] ); + m_ui->m_scaleZ->setChecked( m_displayCurve[9] ); + } +} +#undef registerValue +#undef registerValue + +void KeyFrameEditorFrame::updateCursorSpin() { + if ( m_ui == nullptr ) { return; } + auto times = m_value->getTimeSchedule(); + if ( times.find( m_cursor ) != times.end() ) + { + m_ui->m_currentTime_dsb->setStyleSheet( "background-color: yellow" ); + m_ui->m_deleteKeyframe->setEnabled( true ); + } + else + { + m_ui->m_currentTime_dsb->setStyleSheet( "background-color: #5555ff" ); + m_ui->m_deleteKeyframe->setEnabled( false ); + } + m_ui->m_currentTime_dsb->setValue( double( m_cursor ) ); +} + +} // namespace Ra::GuiBase diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrame.h b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrame.h new file mode 100644 index 00000000000..6e25f9634e4 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrame.h @@ -0,0 +1,215 @@ +#ifndef RADIUMENGINE_KEYFRAME_EDITOR_FRAME_H +#define RADIUMENGINE_KEYFRAME_EDITOR_FRAME_H + +#include +#include +#include +#include +#include +#include + +#include + +namespace Ra::Core::Animation { +class KeyFramedValueBase; +} + +namespace Ra::GuiBase { +class KeyFrameEditorScrollArea; +} + +namespace Ui { +class KeyFrameEditor; +} + +namespace Ra::GuiBase { +class KeyFrameEditor; + +/** + * The KeyFrameEditorFrame manages the display of keyframed values in the KeyFrameEditor. + * It also manages the update of the KeyFrameEditor UI. + */ +class KeyFrameEditorFrame : public QFrame +{ + Q_OBJECT + public: + friend class KeyFrameEditor; + /// Shorthand for KeyFramedValue. + using KeyFrame = Ra::Core::Animation::KeyFramedValueBase; + + explicit KeyFrameEditorFrame( QWidget* parent = nullptr ); + ~KeyFrameEditorFrame() override; + + /// Set the Editor main UI for posterior update. + void setEditorUi( Ui::KeyFrameEditor* ui ); + + /// Set the duration of the KeyFrameEditor's playzone. + void setDuration( Scalar time ); + + /// Registers the KeyFramedValue begin edited. + void setKeyFramedValue( KeyFrame* frame ); + + /** + * Return the set of points in time where a KeyFrame is defined for the + * currently edited KeyFramedValue. + */ + std::set getKeyFrames() const; + + public slots: + /** + * Set the current time of the KeyFrameEditor's playzone to the given time. + * \param time the new current time. + * \param internal whether the call has been made internally by the KeyFrameEditor, + * e.g. through the UI; or externally through KeyFrameEditor::onChangeEnd() + * or through the TimelineFrameSelector. + * \note If \p internal is set to true, the signal cursorChanged() will be + * emitted. + */ + void onChangeCursor( Scalar time, bool internal = true ); + + /** + * Set the current time to the given time and update the display of the + * edited KeyFramedValue at the given time. + */ + void onUpdateKeyFrames( Scalar currentTime ); + + signals: + /** + * Emitted when the current time has been changed though + * - the user changing the current time in the KeyFrameEditor, + * - the user offsetting a set of KeyFrames, + * - the user deleting an entire timezone. + */ + void cursorChanged( Scalar time ); + + /// Emitted when the user changes a KeyFrame's value. + void keyFrameChanged( Scalar time ); + + /// Emitted when the user adds a KeyFrame to the KeyFramedValue. + void keyFrameAdded( Scalar time ); + + /** + * Emitted when a KeyFrame has been deleted through + * - the user deleting a single KeyFrame. + * - the user deleting an entire timezone containing a KeyFrame. + * \note Emitted for each suppressed KeyFrame. + */ + void keyFrameDeleted( Scalar time ); + + /// Emitted when the user changes a KeyFrame's time. + void keyFrameMoved( Scalar time0, Scalar time1 ); + + /** + * Emitted when a KeyFrame has been added through + * - the user offsetting a set of keyframes. + * - the user deleting an entire timezone preceding KeyFrames. + */ + void keyFramesMoved( Scalar time, Scalar offset ); + + /// Emitted when the editor has been updated. + void updated(); + + private slots: + /** + * Emits keyFrameAdded() and then calls onUpdateKeyFrames(). + * \note If there already is a Keyframe at the current time, does nothing. + */ + void onAddKeyFrame(); + + /** + * Emits keyFrameDeleted() and then calls onUpdateKeyFrames(). + * \note If there is no Keyframe at the current time, does nothing. + */ + void onDeleteKeyFrame(); + + /** + * Emits keyFrameMoved() and then calls onUpdateKeyFrames(). + * \note If \p time0 == \p time1, does nothing. + */ + void onMoveKeyFrame( Scalar time0, Scalar time1 ); + + /** + * Emits keyFramesMoved() and then calls onUpdateKeyFrames(). + * \note If \p offset == 0, or there is no KeyFrame at \p time, does nothing. + * \note If \p offset is negative and offsetting the KeyFrame at \p time covers + * the current time, this one will be offset too and cursorChanged() will + * be emitted. + */ + void onMoveKeyFrames( Scalar time, Scalar offset ); + + /** + * Emits keyFrameMoved for each KeyFrame after \p time2, + * keyFrameDeleted for each KeyFrame between time and time2, + * then emits cursorChanged then calls onUpdateKeyFrames(). + */ + void deleteZone( Scalar time, Scalar time2 ); + + /** + * Calls onChangeCursor() with the time of the KeyFrame right before m_cursor + * if it exists, does nothing otherwise. + */ + void onSetCursorToPreviousKeyFrame(); + + /** + * Calls onChangeCursor() with the time of the KeyFrame right after m_cursor + * if it exists, does nothing otherwise. + */ + void onSetCursorToNextKeyFrame(); + + protected: + virtual void paintEvent( QPaintEvent* event ) override; + + void mousePressEvent( QMouseEvent* event ) override; + void mouseMoveEvent( QMouseEvent* event ) override; + void mouseReleaseEvent( QMouseEvent* event ) override; + + private: + /** + * Tries to stick the cursor from \p time to the nearest KeyFrame, + * depending on the zoom level. + * \returns the nearest KeyFrame if could stick, time otherwise. + */ + Scalar nearestStep( Scalar time ) const; + + /** + * Registers the KeyFramed value channels for display update. + * \param newValue whether the Keyframed value has changed or not. + */ + void registerKeyFrames( bool newValue = false ); + + /// Updates the "cursor" SpinBox. + void updateCursorSpin(); + + private: + /// The currently edited KeyFramedValue. + KeyFrame* m_value{nullptr}; + + /// The current time of the Timeline's playzone. + Scalar m_cursor; + + /// The display state of the KeyFramed value channels. + std::array m_displayCurve = + {true, true, true, true, true, true, true, true, true, true}; + + /// Whether the user is editing values or not. + bool m_mouseLeftClicked{false}; + + /// The type for KeyFramedValue edition control points. + using CurveControlPoints = std::vector; + + /// The per-channel sets of control points. + std::array m_curveControlPoints; + + /// The current control point. + Ra::Core::Vector2i m_currentControlPoint{-1, -1}; + + /// The QPainter to display all the data. + QPainter* m_painter{nullptr}; + + /// The KeyFrameEditor UI. + Ui::KeyFrameEditor* m_ui{nullptr}; +}; + +} // namespace Ra::GuiBase + +#endif // RADIUMENGINE_KEYFRAME_EDITOR_FRAME_H diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrameScale.cpp b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrameScale.cpp new file mode 100644 index 00000000000..419c3063900 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrameScale.cpp @@ -0,0 +1,52 @@ +#include + +#include +#include + +#include + +#include +#include + +namespace Ra::GuiBase { + +KeyFrameEditorFrameScale::KeyFrameEditorFrameScale( QWidget* parent ) : QFrame( parent ) {} + +void KeyFrameEditorFrameScale::setScrollArea( KeyFrameEditorScrollArea* scrollArea ) { + m_scrollArea = scrollArea; +} + +void KeyFrameEditorFrameScale::setEditorFrame( KeyFrameEditorFrame* editorFrame ) { + m_editorFrame = editorFrame; +} + +void KeyFrameEditorFrameScale::paintEvent( QPaintEvent* ) { + if ( m_scrollArea == nullptr || m_editorFrame == nullptr ) { return; } + + QPainter painter( this ); + painter.setRenderHint( QPainter::HighQualityAntialiasing ); + + int offset = m_scrollArea->horizontalScrollBar()->value(); + + // DRAW FRAME SCALE + painter.setPen( QPen( Qt::lightGray ) ); + Scalar frameDuration = 1_ra / TIMELINE_FPS; + Scalar nbFrame = m_scrollArea->getMaxTime() / frameDuration; + Scalar pixPerSec = m_scrollArea->getPixPerSec(); + Scalar zeroTime = m_scrollArea->getZeroTime(); + for ( int i = 0; i < nbFrame; i++ ) + { + int x = static_cast( i * frameDuration * pixPerSec + zeroTime ); + painter.drawLine( x - offset, 0, x - offset, height() ); + } + + // DRAW KEYFRAMES + painter.setPen( QPen( QColor( 255, 255, 0, 255 ), 3 ) ); + for ( Scalar keyFrame : m_editorFrame->getKeyFrames() ) + { + int xKeyFrame = static_cast( keyFrame * pixPerSec + zeroTime ); + painter.drawLine( xKeyFrame - offset, height() / 2, xKeyFrame - offset, height() ); + } +} + +} // namespace Ra::GuiBase diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrameScale.h b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrameScale.h new file mode 100644 index 00000000000..fd1c7115278 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorFrameScale.h @@ -0,0 +1,42 @@ +#ifndef RADIUMENGINE_KEYFRAME_EDITOR_FRAMESCALE_H +#define RADIUMENGINE_KEYFRAME_EDITOR_FRAMESCALE_H + +#include + +namespace Ra::GuiBase { +class KeyFrameEditorFrame; +class KeyFrameEditorScrollArea; +} // namespace Ra::GuiBase + +namespace Ra::GuiBase { + +/** + * The KeyFrameEditorFrameScale class displays, along the x-axis, + * the KeyFrames for the KeyFrameEditorFrame it belongs to. + */ +class KeyFrameEditorFrameScale : public QFrame +{ + Q_OBJECT + public: + explicit KeyFrameEditorFrameScale( QWidget* parent = nullptr ); + + /// Set the KeyFrameEditorFrame for which to display the KeyFrames. + void setEditorFrame( KeyFrameEditorFrame* editorFrame ); + + /// Set the KeyFrameEditorScrollArea to display the KeyFrames in. + void setScrollArea( KeyFrameEditorScrollArea* scrollArea ); + + protected: + virtual void paintEvent( QPaintEvent* event ) override; + + private: + /// The KeyFrameEditorFrame. + KeyFrameEditorFrame* m_editorFrame{nullptr}; + + /// The KeyFrameEditorScrollArea. + KeyFrameEditorScrollArea* m_scrollArea{nullptr}; +}; + +} // namespace Ra::GuiBase + +#endif // RADIUMENGINE_KEYFRAME_EDITOR_FRAMESCALE_H diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorScrollArea.cpp b/src/GuiBase/KeyFrameEditor/KeyFrameEditorScrollArea.cpp new file mode 100644 index 00000000000..111f7f7db22 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorScrollArea.cpp @@ -0,0 +1,256 @@ +#include + +#include + +#include +#include + +#include +#include + +namespace Ra::GuiBase { + +KeyFrameEditorScrollArea::KeyFrameEditorScrollArea( QWidget* parent ) : QScrollArea( parent ) {} + +int KeyFrameEditorScrollArea::getNbIntervalTime() { + return m_nbIntervalTime; +} + +Scalar KeyFrameEditorScrollArea::getStepTime() { + return m_stepTime; +} + +Scalar KeyFrameEditorScrollArea::getPixPerSec() { + return m_pixPerTime; +} + +Scalar KeyFrameEditorScrollArea::getZeroTime() { + return m_zeroTime; +} + +Scalar KeyFrameEditorScrollArea::getMaxTime() { + return m_maxTime; +} + +void KeyFrameEditorScrollArea::setMaxTime( Scalar maxTime ) { + m_maxTime = maxTime; +} + +int KeyFrameEditorScrollArea::getNbIntervalValue() { + return m_nbIntervalValue; +} + +Scalar KeyFrameEditorScrollArea::getStepValue() { + return m_stepValue; +} + +Scalar KeyFrameEditorScrollArea::getPixPerValue() { + return m_pixPerValue; +} + +Scalar KeyFrameEditorScrollArea::getZeroValue() { + return m_zeroValue; +} + +Scalar KeyFrameEditorScrollArea::getMaxValue() { + return m_maxValue; +} + +void KeyFrameEditorScrollArea::onDrawRuler( int width, int height ) { + int iStep = 0; + while ( iStep < s_nbSteps && width * s_steps[iStep] < 50 * m_maxTime ) + iStep++; + + if ( iStep == s_nbSteps ) { return; } + m_stepTime = s_steps[iStep]; + + iStep = 0; + while ( iStep < s_nbSteps && height * s_steps[iStep] < 50 * m_maxValue ) + iStep++; + + if ( iStep == s_nbSteps ) { return; } + m_stepValue = s_steps[iStep]; + + emit stepChanged( m_stepTime ); + + m_nbIntervalTime = int( std::ceil( m_maxTime / m_stepTime ) ) + 2; + m_pixPerTime = ( Scalar( width ) / m_nbIntervalTime ) / m_stepTime; + m_zeroTime = m_pixPerTime * m_stepTime; + + m_nbIntervalValue = int( std::ceil( m_maxValue / m_stepValue ) ) + 2; + m_pixPerValue = ( Scalar( height ) / m_nbIntervalValue ) / m_stepValue; + m_zeroValue = height / 2; // m_pixPerValue * m_stepValue; + + widget()->setMinimumWidth( width ); + widget()->setMinimumHeight( height ); + + update(); +} + +void KeyFrameEditorScrollArea::keyPressEvent( QKeyEvent* event ) { + switch ( event->key() ) + { + + case Qt::Key_Space: + emit togglePlayPause(); + break; + + case Qt::Key_Delete: + emit removeKeyFrame(); + break; + + case Qt::Key_I: + if ( event->modifiers() & Qt::Modifier::SHIFT ) { emit removeKeyFrame(); } + else + { emit addKeyFrame(); } + break; + + case Qt::Key_Left: + emit previousKeyFrame(); + break; + + case Qt::Key_Right: + emit nextKeyFrame(); + break; + + case Qt::Key_Z: + if ( event->modifiers() & Qt::Modifier::CTRL ) + { + if ( event->modifiers() & Qt::Modifier::SHIFT ) { emit redo(); } + else + { emit undo(); } + } + break; + + case Qt::Key_U: + emit undo(); + break; + + case Qt::Key_R: + emit redo(); + } +} + +void KeyFrameEditorScrollArea::wheelEvent( QWheelEvent* event ) { + const auto finishZoom = [this, event]( int w, int h ) { + double hScroll = horizontalScrollBar()->value(); + double x = event->x(); + double time = ( hScroll + x - double( m_zeroTime ) ) / double( m_pixPerTime ); + + double vScroll = verticalScrollBar()->value(); + double y = event->y(); + double value = ( vScroll + y - double( m_zeroValue ) ) / double( m_pixPerValue ); + + onDrawRuler( w, h ); + + // ruler values may have changed + double a = time * double( m_pixPerTime ) + double( m_zeroTime ); + double hScrollAfterProjection = a - x; + horizontalScrollBar()->setValue( int( hScrollAfterProjection ) ); + + double b = value * double( m_pixPerValue ) + double( m_zeroValue ); + double vScrollAfterProjection = b - y; + verticalScrollBar()->setValue( int( vScrollAfterProjection ) ); + }; + + int ry = event->angleDelta().ry(); + bool altDown = event->modifiers() & Qt::Modifier::ALT; + bool ctrlDown = event->modifiers() & Qt::Modifier::CTRL; + bool shiftDown = event->modifiers() & Qt::Modifier::SHIFT; + // asymetric zoom + if ( altDown ) + { + ry = event->angleDelta().rx(); + int newRulerWidth = widget()->minimumWidth(); + int newRulerHeight = widget()->minimumHeight(); + if ( shiftDown ) + { + newRulerWidth = int( widget()->minimumWidth() + + ry * TIMELINE_ZOOM_SPEED * widget()->minimumWidth() / width() ); + if ( newRulerWidth <= width() - 2 ) + { + if ( widget()->minimumWidth() == width() - 2 ) { return; } + else + { newRulerWidth = width() - 2; } + } + } + else + { + newRulerHeight = int( widget()->minimumHeight() + + ry * TIMELINE_ZOOM_SPEED * widget()->minimumHeight() / height() ); + if ( newRulerHeight <= height() - 2 ) + { + if ( widget()->minimumHeight() == height() - 2 ) { return; } + else + { newRulerHeight = height() - 2; } + } + } + + finishZoom( newRulerWidth, newRulerHeight ); + } + // scroll up/down + else if ( shiftDown ) + { + verticalScrollBar()->setValue( + static_cast( verticalScrollBar()->value() + ry * TIMELINE_SLIDE_SPEED ) ); + } + // scroll left/right + else if ( ctrlDown ) + { + horizontalScrollBar()->setValue( + static_cast( horizontalScrollBar()->value() + ry * TIMELINE_SLIDE_SPEED ) ); + } + // symetric zoom in/out + else + { + const int minWidth = widget()->minimumWidth(); + int newRulerWidth = + static_cast( minWidth + ry * TIMELINE_ZOOM_SPEED * minWidth / width() ); + if ( newRulerWidth <= width() - 2 ) + { + if ( minWidth == width() - 2 ) { newRulerWidth = minWidth; } + else + { newRulerWidth = width() - 2; } + } + + const int minHeight = widget()->minimumHeight(); + int newRulerHeight = + static_cast( minHeight + ry * TIMELINE_ZOOM_SPEED * minHeight / height() ); + if ( newRulerHeight <= height() - 2 ) + { + if ( minHeight == height() - 2 ) { newRulerHeight = minHeight; } + else + { newRulerHeight = height() - 2; } + } + + finishZoom( newRulerWidth, newRulerHeight ); + } + event->accept(); // parent is animTimeline (root) with non event catching + update(); +} + +void KeyFrameEditorScrollArea::mousePressEvent( QMouseEvent* event ) { + if ( event->button() == Qt::MiddleButton ) + { + setCursor( Qt::SizeAllCursor ); + m_mousePosX = event->x(); + m_mousePosY = event->y(); + m_sliderPosX = horizontalScrollBar()->value(); + m_sliderPosY = verticalScrollBar()->value(); + } +} + +void KeyFrameEditorScrollArea::mouseReleaseEvent( QMouseEvent* event ) { + if ( event->button() == Qt::MiddleButton ) { setCursor( Qt::ArrowCursor ); } +} + +void KeyFrameEditorScrollArea::mouseMoveEvent( QMouseEvent* event ) { + if ( event->buttons() & Qt::MiddleButton ) + { + horizontalScrollBar()->setValue( ( m_sliderPosX + m_mousePosX - event->x() ) ); + verticalScrollBar()->setValue( ( m_sliderPosY + m_mousePosY - event->y() ) ); + update(); + } +} + +} // namespace Ra::GuiBase diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorScrollArea.h b/src/GuiBase/KeyFrameEditor/KeyFrameEditorScrollArea.h new file mode 100644 index 00000000000..cfe18c2ecff --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorScrollArea.h @@ -0,0 +1,132 @@ +#ifndef RADIUMENGINE_KEYFRAME_EDITOR_SCROLLAREARULER_H +#define RADIUMENGINE_KEYFRAME_EDITOR_SCROLLAREARULER_H + +#include + +#include + +namespace Ra::GuiBase { + +/** + * The KeyFrameEditorScrollArea class manages the ScrollArea for the KeyFrameEditor. + * + * It offers display info for its sub-widgets (size, zoom level, scroll shift...). + */ +class KeyFrameEditorScrollArea : public QScrollArea +{ + Q_OBJECT + public: + explicit KeyFrameEditorScrollArea( QWidget* parent = nullptr ); + + /// Set the end<\b> of the Timeline's playzone. + void setMaxTime( Scalar maxTime ); + + /// \returns the end<\b> of the Timeline's playzone. + Scalar getMaxTime(); + + /// \return the step between two time scale graduations. + Scalar getStepTime(); + + /// \returns the number of time scale graduations. + int getNbIntervalTime(); + + /// \returns the number of pixels used to display 1 second on the time scale. + Scalar getPixPerSec(); + + /// \returns the pixel corresponding to time 0. + Scalar getZeroTime(); + + /// Return the maximal value of the edited KeyFramedValue. + Scalar getMaxValue(); + + /// \return the step between two value scale graduations. + Scalar getStepValue(); + + /// \returns the number of value scale graduations. + int getNbIntervalValue(); + + /// \returns the number of pixels used to display 1 second on the value scale. + Scalar getPixPerValue(); + + /// \returns the pixel corresponding to value 0. + Scalar getZeroValue(); + + signals: + /// Emitted when zoomed in/out or resized. + void stepChanged( Scalar step ); + + /// Emitted when is pressed. + void addKeyFrame(); + + /// Emitted when / + is pressed. + void removeKeyFrame(); + + /// Emitted when is pressed or + . + void previousKeyFrame(); + + /// Emitted when is pressed or + . + void nextKeyFrame(); + + /// Emitted when is pressed. + void togglePlayPause(); + + /// Emitted when + / key is pressed. + void undo(); + + /// Emitted when + + / key is pressed. + void redo(); + + public slots: + /// Force redraw with the given width. + void onDrawRuler( int width, int height ); + + protected: + void keyPressEvent( QKeyEvent* event ) override; + + void wheelEvent( QWheelEvent* event ) override; + + void mousePressEvent( QMouseEvent* event ) override; + void mouseReleaseEvent( QMouseEvent* event ) override; + void mouseMoveEvent( QMouseEvent* event ) override; + + private: + int m_mousePosX; ///< x coordinate of the mouse on mouse middle click. + int m_mousePosY; ///< y coordinate of the mouse on mouse middle click. + int m_sliderPosX; ///< x coordinate of the slider on mouse middle click. + int m_sliderPosY; ///< y coordinate of the slider on mouse middle click. + + Scalar m_maxTime{200}; ///< end<\b> of the Timeline's playzone. + Scalar m_stepTime; ///< Step between two time scale graduations. + int m_nbIntervalTime{0}; ///< Number of time scale graduations. + Scalar m_pixPerTime; ///< Number of pixels used to display 1 second on the scale. + Scalar m_zeroTime; ///< Pixel corresponding to time 0. + + Scalar m_maxValue{200}; ///< Maximal extent of a KeyFrame value. + Scalar m_stepValue; ///< Step between two value scale graduations. + int m_nbIntervalValue{0}; ///< Number of value scale graduations. + Scalar + m_pixPerValue; ///< Number of pixels used to display a value difference of 1 on the scale. + Scalar m_zeroValue; ///< Pixel corresponding to time 0. + + /// Number of possible steps. + static constexpr int s_nbSteps = 13; + + /// Possible steps. + static constexpr Scalar s_steps[s_nbSteps] = {0.01_ra, + 0.02_ra, + 0.05_ra, + 0.1_ra, + 0.2_ra, + 0.5_ra, + 1.0_ra, + 2.0_ra, + 5.0_ra, + 10.0_ra, + 20.0_ra, + 50.0_ra, + 100.0_ra}; +}; + +} // namespace Ra::GuiBase + +#endif // RADIUMENGINE_KEYFRAME_EDITOR_SCROLLAREARULER_H diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorTimeScale.cpp b/src/GuiBase/KeyFrameEditor/KeyFrameEditorTimeScale.cpp new file mode 100644 index 00000000000..aeacbb5572e --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorTimeScale.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include + +#include + +namespace Ra::GuiBase { + +KeyFrameEditorTimeScale::KeyFrameEditorTimeScale( QWidget* parent ) : QFrame( parent ) {} + +void KeyFrameEditorTimeScale::setScrollArea( KeyFrameEditorScrollArea* scrollArea ) { + m_scrollArea = scrollArea; +} + +void KeyFrameEditorTimeScale::paintEvent( QPaintEvent* ) { + if ( m_scrollArea == nullptr ) { return; } + + QPainter painter( this ); + painter.setRenderHint( QPainter::HighQualityAntialiasing ); + + int offset = m_scrollArea->horizontalScrollBar()->value(); + + Scalar pixPerSec = m_scrollArea->getPixPerSec(); + Scalar stepTime = m_scrollArea->getStepTime(); + for ( int i = 1; i < m_scrollArea->getNbIntervalTime(); i++ ) + { + int x = int( pixPerSec * stepTime * i ); + QString time = QString::number( double( ( i - 1 ) * stepTime ) ); + int dec = time.size() * 3; // 6 / 2 + painter.drawText( x - dec - offset, 15, time ); + } +} + +} // namespace Ra::GuiBase diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorTimeScale.h b/src/GuiBase/KeyFrameEditor/KeyFrameEditorTimeScale.h new file mode 100644 index 00000000000..ca500c5e689 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorTimeScale.h @@ -0,0 +1,35 @@ +#ifndef RADIUMENGINE_KEY_FRAME_EDITOR_TIMESCALE_H +#define RADIUMENGINE_KEY_FRAME_EDITOR_TIMESCALE_H + +#include + +namespace Ra::GuiBase { +class KeyFrameEditorScrollArea; +} // namespace Ra::GuiBase + +namespace Ra::GuiBase { + +/** + * The KeyFrameEditorTimeScale class displays the scale, along the x-axis, + * of the KeyFrameEditorScrollArea it belongs to. + */ +class KeyFrameEditorTimeScale : public QFrame +{ + Q_OBJECT + public: + explicit KeyFrameEditorTimeScale( QWidget* parent = nullptr ); + + /// Set the KeyFrameEditorScrollArea for which to display the scale. + void setScrollArea( KeyFrameEditorScrollArea* scrollArea ); + + protected: + virtual void paintEvent( QPaintEvent* event ) override; + + private: + /// The KeyFrameEditorScrollArea. + KeyFrameEditorScrollArea* m_scrollArea{nullptr}; +}; + +} // namespace Ra::GuiBase + +#endif // RADIUMENGINE_KEY_FRAME_EDITOR_TIMESCALE_H diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorValueScale.cpp b/src/GuiBase/KeyFrameEditor/KeyFrameEditorValueScale.cpp new file mode 100644 index 00000000000..57d4178ff4e --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorValueScale.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include + +#include + +namespace Ra::GuiBase { + +KeyframeEditorValueScale::KeyframeEditorValueScale( QWidget* parent ) : QFrame( parent ) {} + +void KeyframeEditorValueScale::setScrollArea( KeyFrameEditorScrollArea* scrollArea ) { + m_scrollArea = scrollArea; +} + +void KeyframeEditorValueScale::paintEvent( QPaintEvent* ) { + if ( m_scrollArea == nullptr ) { return; } + + QPainter painter( this ); + painter.setRenderHint( QPainter::HighQualityAntialiasing ); + + int offset = m_scrollArea->verticalScrollBar()->value(); + + Scalar pixPerValue = m_scrollArea->getPixPerValue(); + Scalar stepValue = m_scrollArea->getStepValue(); + Scalar maxValue = m_scrollArea->getMaxValue(); + for ( int i = 1; i < m_scrollArea->getNbIntervalValue(); i++ ) + { + int y = int( pixPerValue * stepValue * i ); + QString value = QString::number( double( maxValue / 2 - ( i - 1 ) * stepValue ) ); + painter.drawText( 5, y - offset, value ); + } +} + +} // namespace Ra::GuiBase diff --git a/src/GuiBase/KeyFrameEditor/KeyFrameEditorValueScale.h b/src/GuiBase/KeyFrameEditor/KeyFrameEditorValueScale.h new file mode 100644 index 00000000000..27fa5184ba6 --- /dev/null +++ b/src/GuiBase/KeyFrameEditor/KeyFrameEditorValueScale.h @@ -0,0 +1,35 @@ +#ifndef RADIUMENGINE_KEYFRAME_EDITOR_VALUESCALE_H +#define RADIUMENGINE_KEYFRAME_EDITOR_VALUESCALE_H + +#include + +namespace Ra::GuiBase { +class KeyFrameEditorScrollArea; +} // namespace Ra::GuiBase + +namespace Ra::GuiBase { + +/** + * The KeyframeEditorValueScale class displays the Value scale, along the y-axis, + * of the KeyframeEditorScrollArea it belongs to. + */ +class KeyframeEditorValueScale : public QFrame +{ + Q_OBJECT + public: + explicit KeyframeEditorValueScale( QWidget* parent = nullptr ); + + /// Set the KeyframeEditorScrollArea for which to display the scale. + void setScrollArea( KeyFrameEditorScrollArea* scrollArea ); + + protected: + virtual void paintEvent( QPaintEvent* event ) override; + + private: + /// The KeyframeEditorScrollArea. + KeyFrameEditorScrollArea* m_scrollArea{nullptr}; +}; + +} // namespace Ra::GuiBase + +#endif // RADIUMENGINE_KEYFRAME_EDITOR_VALUESCALE_H diff --git a/src/GuiBase/Timeline/Timeline.cpp b/src/GuiBase/Timeline/Timeline.cpp index fbf5bc3f1a9..a3d3be4f923 100644 --- a/src/GuiBase/Timeline/Timeline.cpp +++ b/src/GuiBase/Timeline/Timeline.cpp @@ -18,6 +18,7 @@ #include #include +#include #include using namespace Ra::Core::Utils; @@ -516,10 +517,82 @@ void Timeline::on_comboBox_attribute_currentIndexChanged( const QString& arg1 ) ui->frame_selector->onAddingKeyFrame( t, false ); } m_current = frames; + if ( m_keyFrameEditor != nullptr ) + { + m_keyFrameEditor->setKeyFramedValue( m_current.m_name, m_current.m_value ); + m_keyFrameEditor->onChangeCursor( Scalar( ui->m_cursorSpin->value() ) ); + } } void Timeline::on_pushButton_editAttribute_clicked() { - // TODO + if ( m_keyFrameEditor == nullptr ) + { + m_keyFrameEditor = new KeyFrameEditor( Scalar( ui->m_durationSpin->value() ), this ); + // FROM TIMELINE TO EDITOR + connect( ui->frame_selector, + &TimelineFrameSelector::cursorChanged, + m_keyFrameEditor, + &KeyFrameEditor::onChangeCursor ); + connect( ui->frame_selector, + &TimelineFrameSelector::durationChanged, + m_keyFrameEditor, + &KeyFrameEditor::onChangeDuration ); + + connect( ui->frame_selector, &TimelineFrameSelector::keyFrameAdded, [this]( Scalar t ) { + m_keyFrameEditor->onUpdateKeyFrames( t ); + } ); + connect( ui->frame_selector, &TimelineFrameSelector::keyFrameDeleted, [this]( Scalar t ) { + m_keyFrameEditor->onUpdateKeyFrames( t ); + } ); + connect( + ui->frame_selector, + &TimelineFrameSelector::keyFrameMoved, + [this]( Scalar /*t0*/, Scalar t1 ) { m_keyFrameEditor->onUpdateKeyFrames( t1 ); } ); + connect( ui->frame_selector, + &TimelineFrameSelector::keyFramesMoved, + [this]( Scalar t, Scalar offset ) { + m_keyFrameEditor->onUpdateKeyFrames( t + offset ); + } ); + + // FROM EDITOR TO TIMELINE + connect( + m_keyFrameEditor, &KeyFrameEditor::cursorChanged, this, &Timeline::onChangeCursor ); + connect( m_keyFrameEditor, &KeyFrameEditor::cursorChanged, this, &Timeline::cursorChanged ); + connect( m_keyFrameEditor, &KeyFrameEditor::keyFrameChanged, [this]( double ) { + updateKeyFrames( Scalar( ui->m_cursorSpin->value() ) ); + emit keyFrameChanged( Scalar( ui->m_cursorSpin->value() ) ); + emit cursorChanged( Scalar( ui->m_cursorSpin->value() ) ); + } ); + + connect( m_keyFrameEditor, &KeyFrameEditor::keyFrameAdded, [=]( Scalar t ) { + ui->frame_selector->onAddingKeyFrame( t, false ); + } ); + connect( + m_keyFrameEditor, &KeyFrameEditor::keyFrameAdded, this, &Timeline::onAddingKeyFrame ); + connect( m_keyFrameEditor, &KeyFrameEditor::keyFrameDeleted, [=]( Scalar time ) { + ui->frame_selector->onChangeCursor( time ); // make sure we are on the right spot + ui->frame_selector->onDeletingKeyFrame( false ); + } ); + connect( m_keyFrameEditor, + &KeyFrameEditor::keyFrameDeleted, + this, + &Timeline::onRemovingKeyFrame ); + connect( m_keyFrameEditor, &KeyFrameEditor::keyFrameMoved, [=]( Scalar t0, Scalar t1 ) { + ui->frame_selector->onMoveKeyFrame( t0, t1, false ); + } ); + connect( + m_keyFrameEditor, &KeyFrameEditor::keyFrameMoved, this, &Timeline::onMovingKeyFrame ); + connect( + m_keyFrameEditor, &KeyFrameEditor::keyFramesMoved, [=]( Scalar time, Scalar offset ) { + ui->frame_selector->onMoveKeyFrames( time, offset, false ); + } ); + connect( + m_keyFrameEditor, &KeyFrameEditor::keyFramesMoved, this, &Timeline::onMovingKeyFrames ); + } + + m_keyFrameEditor->setKeyFramedValue( m_current.m_name, m_current.m_value ); + m_keyFrameEditor->onChangeCursor( Scalar( ui->m_cursorSpin->value() ) ); + m_keyFrameEditor->show(); } void Timeline::on_toolButton_help_clicked() { diff --git a/src/GuiBase/Timeline/Timeline.h b/src/GuiBase/Timeline/Timeline.h index 2a101098da4..b054fe06186 100644 --- a/src/GuiBase/Timeline/Timeline.h +++ b/src/GuiBase/Timeline/Timeline.h @@ -24,6 +24,8 @@ class Timeline; } namespace Ra::GuiBase { +class KeyFrameEditor; + /// The KeyFrameManipulator class provides a way to call callback functions on /// KeyFramedValues upon keyframe insertion, modification or a modification of /// the current time. @@ -293,6 +295,9 @@ class RA_GUIBASE_API Timeline : public QDialog /// The Timeline UI. Ui::Timeline* ui; + /// The KeyFrame Editor. + KeyFrameEditor* m_keyFrameEditor{nullptr}; + /// The per-Entity keyframes. std::map> m_entityKeyFrames; diff --git a/src/GuiBase/Timeline/TimelineFrameSelector.h b/src/GuiBase/Timeline/TimelineFrameSelector.h index 9669deb007d..42a7c430cfe 100644 --- a/src/GuiBase/Timeline/TimelineFrameSelector.h +++ b/src/GuiBase/Timeline/TimelineFrameSelector.h @@ -66,7 +66,8 @@ class TimelineFrameSelector : public QFrame * Set the current time of the Timeline's playzone to the given time. * \param time the new current time. * \param internal whether the call has been made internally by the Timeline, - * e.g. through the UI; or externally through Timeline::onChangeEnd(). + * e.g. through the UI; or externally through Timeline::onChangeEnd() + * or through the KeyFrameEditor. * \note If \p internal is set to true, the signal cursorChanged() will be * emitted. */ @@ -78,7 +79,7 @@ class TimelineFrameSelector : public QFrame * \param time the time to add a KeyFrame at. * \param internal whether the call has been made internally by the Timeline, * e.g. through the UI; or externally, e.g. by setting Keyrames in the - * Timeline. + * Timeline or by the KeyFrame editor. * \note If \p time is set to its default value, then it is considered as * the current time. * \note If there already is a KeyFrame at time \p time, then it will be @@ -94,7 +95,8 @@ class TimelineFrameSelector : public QFrame /** * Removes the KeyFrame at the current time, if any. * \param internal whether the call has been made internally by the Timeline, - * e.g. through the UI; or externally. + * e.g. through the UI; or externally, e.g. by removing KeyFrames in + * the KeyFrame editor. * \note If \p internal is set to true, the signal keyFrameDeleted() will be * emitted if a KeyFrame has been effectively removed. */ @@ -105,7 +107,8 @@ class TimelineFrameSelector : public QFrame * \param time0 the time to remove the KeyFrame at. * \param time1 the time to add the KeyFrame at. * \param internal whether the call has been made internally by the Timeline, - * e.g. through the UI; or externally. + * e.g. through the UI; or externally, e.g. by moving KeyFrames in the + * KeyFrame editor. * \note If \p time0 == \p time1, does nothing. * \note If \p internal is set to true, the signal keyFrameMoved() will be * emitted if the KeyFrame has been effectively moved. @@ -117,7 +120,8 @@ class TimelineFrameSelector : public QFrame * \param time the time from which to offset KeyFrames. * \param offset the offset to offset KeyFrames of. * \param internal whether the call has been made internally by the Timeline, - * e.g. through the UI; or externally . + * e.g. through the UI; or externally, e.g. by moving KeyFrames in the + * KeyFrame editor. * \note If \p offset == 0, or \p time has no KeyFrame, does nothing. * \note If \p internal is set to true, the signal keyFramesMoved() will be * emitted if the KeyFrames have been effectively moved.