#include "externals.hh" #include "common.hh" #include "c-sync.hh" #include "c-syncedit.hh" #include "ui.hh" #include "ui-app.hh" #include "ui-overrides.hh" #include "ui-sequencer.hh" #include "ui-sync.hh" #include "ui-utilities.hh" #include "ui-imgui-sdl.hh" #define IMGUI_DEFINE_MATH_OPERATORS #include using namespace ebcl; namespace { /*= VARIOUS HELPERS ==========================================================*/ bool FakeTab_( char const* const name , const bool disabled , const float width = 0.f ) { using namespace ImGui; if ( disabled ) { PushDisabled( ); } const bool rv( Button( name , ImVec2{ width , 0.f } ) ); if ( disabled ) { PopDisabled( ); } return rv; } void TimeToString_( char* const buffer , const size_t bSize , const float time ) noexcept { const float msf{ fmod( time , 1.f ) }; const float sf{ fmod( time - msf , 60.f ) }; snprintf( buffer , bSize , "%02d:%02d.%03d" , uint32_t( ( time - msf - sf ) / 60.f ) , uint32_t( sf ) , uint32_t( msf * 1000.f ) ); } /*= T_ChangeDurationDialog_ ==================================================*/ class T_ChangeDurationDialog_ : public A_ModalDialog { private: const uint32_t units0_; const float uSize0_; const uint32_t uPerMinute0_; uint32_t units_; float uSize_; uint32_t uPerMinute_; bool scale_{ true }; protected: uint8_t drawDialog( ) noexcept override; bool onButton( uint8_t id ) noexcept override; public: T_ChangeDurationDialog_( uint32_t units , float uSize ) noexcept; }; /*----------------------------------------------------------------------------*/ T_ChangeDurationDialog_::T_ChangeDurationDialog_( const uint32_t units , const float uSize ) noexcept : A_ModalDialog{ "Set demo duration...##duration-dialog" } , units0_{ units } , uSize0_{ std::max( 1.f / 60.f , uSize ) } , uPerMinute0_{ uint32_t( ImClamp( roundf( 60.f / uSize0_ ) , 1.f , 3600.f ) ) } , units_{ units0_ } , uSize_{ uSize0_ } , uPerMinute_{ uPerMinute0_ } { setInitialSize( 300.f , 180.f ); addButton( "OK" ); addButton( "Cancel" ); } uint8_t T_ChangeDurationDialog_::drawDialog( ) noexcept { using namespace ImGui; int tUnits( units_ ); if ( DragInt( "Duration##units" , &tUnits , .1f , 1 , INT32_MAX , "%.0f unit(s)" ) ) { units_ = uint32_t( std::min( INT32_MAX , std::max( 1 , tUnits ) ) ); } float tDuration( units_ * uSize_ ); if ( DragFloat( "##seconds" , &tDuration , 1.f , .001f , FLT_MAX , "%.3f second(s)" ) ) { units_ = std::min( uint32_t( INT32_MAX ) , std::max( 1u , uint32_t( roundf( tDuration / uSize_ ) ) ) ); } Separator( ); int tUsize( floorf( uSize_ * 1000.f ) ); if ( SliderInt( "Units" , &tUsize , 16 , 2000 , "%.0f ms" ) ) { const float pDur{ uSize_ * units_ }; uSize_ = std::min( 2.f , std::max( 1.f / 60.f , .001f * tUsize ) ); uPerMinute_ = roundf( 60.f / uSize_ ); units_ = uint32_t( roundf( pDur / uSize_ ) ); } int tPerMin( uPerMinute_ ); if ( SliderInt( "Units/minute" , &tPerMin , 30 , 3600 ) ) { const float pDur{ uSize_ * units_ }; uPerMinute_ = std::max( 30u , std::min( 3600u , uint32_t( tPerMin ) ) ); uSize_ = 60.f / uPerMinute_; units_ = uint32_t( roundf( pDur / uSize_ ) ); } if ( uPerMinute0_ == uPerMinute_ ) { PushDisabled( ); } Checkbox( "Scale curves" , &scale_ ); if ( uPerMinute0_ == uPerMinute_ ) { PopDisabled( ); } const bool eo{ units_ != units0_ || uPerMinute_ != uPerMinute0_ }; return eo ? 3 : 2; } bool T_ChangeDurationDialog_::onButton( const uint8_t button ) noexcept { if ( button == 0 ) { SyncEditor::SetDuration( units_ , uPerMinute_ != uPerMinute0_ ? uSize_ : uSize0_ , scale_ ); } return true; } /*= T_PointData_ =============================================================*/ using T_TempCurveStorage_ = T_AutoArray< T_SyncCurve , 16 >; class T_PointData_ : public sov::A_SyncData { private: T_Optional< T_SyncTrackId > const& selId_; T_Optional< uint32_t > const& selSegment_; T_Optional< uint32_t > const& selPoint_; T_TempCurveStorage_& cOriginals_; T_TempCurveStorage_& cCopies_; T_String useId_; uint32_t useSegment_; uint32_t usePoint_; A_SyncOverride* ovr_; public: T_PointData_( T_Optional< T_SyncTrackId > const& selId , T_Optional< uint32_t > const& selSegment , T_Optional< uint32_t > const& selPoint , T_TempCurveStorage_& cOriginals , T_TempCurveStorage_& cCopies ) noexcept : selId_( selId ) , selSegment_( selSegment ) , selPoint_( selPoint ) , cOriginals_( cOriginals ) , cCopies_( cCopies ) { } void use( ) noexcept; float operator[]( uint32_t index ) const noexcept override; bool set( uint32_t index , float value ) noexcept override; T_OwnPtr< sov::A_SyncData > clone( ) const noexcept override; }; /*----------------------------------------------------------------------------*/ void T_PointData_::use( ) noexcept { assert( selId_ && selSegment_ && selPoint_ ); assert( selId_->isOverride ); useId_ = selId_->id; useSegment_ = *selSegment_; usePoint_ = *selPoint_; ovr_ = Common::Sync( ).getOverride( useId_ ); } float T_PointData_::operator[]( const uint32_t index ) const noexcept { if ( !( selId_ && selSegment_ && selPoint_ && selId_->isOverride ) || useId_ != selId_->id || useSegment_ != *selSegment_ || usePoint_ != *selPoint_ ) { return 0.f; } auto const& iNames{ ovr_->inputNames( ) }; if ( cCopies_.size( ) != iNames.size( ) || cCopies_[ 0 ].name != iNames[ 0 ] ) { auto const& curve{ *Common::Sync( ).getCurve( iNames[ index ] ) }; return curve.segments[ useSegment_ ].values[ usePoint_ ]; } return cCopies_[ index ].segments[ useSegment_ ].values[ usePoint_ ]; } bool T_PointData_::set( const uint32_t index , const float value ) noexcept { if ( !( selId_ && selSegment_ && selPoint_ && selId_->isOverride ) || useId_ != selId_->id || useSegment_ != *selSegment_ || usePoint_ != *selPoint_ ) { return false; } auto const& iNames{ ovr_->inputNames( ) }; const auto nn{ iNames.size( ) }; auto& sync{ Common::Sync( ) }; if ( cCopies_.size( ) != nn || cCopies_[ 0 ].name != iNames[ 0 ] ) { cOriginals_.clear( ); cCopies_.clear( ); for ( auto i = 0u ; i < nn ; i ++ ) { auto const* const c{ sync.getCurve( iNames[ i ] ) }; cOriginals_.add( *c ); cCopies_.add( *c ); } } cCopies_[ index ].segments[ useSegment_ ].values[ usePoint_ ] = value; sync.setCurve( cCopies_[ index ] ); return true; } T_OwnPtr< sov::A_SyncData > T_PointData_::clone( ) const noexcept { auto ptr{ NewOwned< T_PointData_ >( selId_ , selSegment_ , selPoint_ , cOriginals_ , cCopies_ ) }; ptr->useId_ = useId_; ptr->useSegment_ = useSegment_; ptr->usePoint_ = usePoint_; return ptr; } /*= T_SyncViewImpl_ ==========================================================*/ struct T_SyncViewImpl_ { static constexpr float SeqHeaderHeight = 24.f; static constexpr float BarWidth = 40.f; static constexpr float TrackHeight = 15.f; static constexpr float TrackPadding = 2.f; static constexpr float TrackLabelHeight = 15.f; static constexpr float PointRadius = ( TrackHeight - 2.f ) * .5f; static constexpr float PointRadiusSqr = PointRadius * PointRadius; bool display( ) noexcept; private: // Track display data struct T_TrackDisplay { T_SyncTrackId id; ImRect area; uint32_t dispSegs; uint32_t firstSeg; }; struct T_TrackSegDisplay { uint32_t track; uint32_t seg; ImRect area; uint32_t dispPoints; uint32_t firstPoint; }; struct T_TrackPointDisplay { uint32_t seg; uint32_t index; ImVec2 center; enum { START , MIDDLE , END } mode; }; // Description for mouse locations in the sequencer window enum class E_MousePosType { NONE , TRACK , SEGMENT , POINT }; struct T_MousePos { E_MousePosType type; uint32_t index; }; // Type of sub-windows enum E_SubWindow { SW_NONE , SW_INPUT_SELECTOR , SW_OVERRIDE_SELECTOR , SW_TRACK , SW_SEGMENT , SW_POINT , }; // Type of change in progress enum class E_ChangeType { NONE , POINT_DND , POINT_VALUE , }; // Type of drag-and-drop operation in progress enum class E_DNDType { NONE , // No drag'n'drop CLICK , // Clicked in an area that will not // cause drag'n'drop to happen TIME , // Time being adjusted ZOOM , // Zoom region being selected POINT , // Point being moved }; // Make sure all displayed inputs/overrides still exist void checkTracks( ) noexcept; // Make sure the currently selected track/segment/point is still valid void checkSelection( ) noexcept; // Display the toolbar void displayToolbar( ) noexcept; //---------------------------------------------------------------------- // Sequencer widget methods void sequencerWidget( ) noexcept; void computeMetrics( float innerWidth ) noexcept; void sequencerHeader( ImRect const& bb ) noexcept; void sequencerBody( ImRect const& bb ) noexcept; void sequencerTracks( ImRect const& container ) noexcept; void sequencerTrack( float& hue , ImRect const& bb , ImRect const& container , T_SyncTrackId const& id , T_SyncCurve const* curve ) noexcept; void displayTooltips( const float time ) noexcept; bool handlePointDrag( const float mPixels , bool mouseDown , bool shiftPressed ) noexcept; T_MousePos getMousePos( ) const noexcept; //---------------------------------------------------------------------- // Track selector void displayTrackSelectorWindow( ) noexcept; void displayInputSelector( ) noexcept; void displayOverrideSelector( ) noexcept; void clearSelectedTracks( ) noexcept; void autoselectTracks( ) noexcept; // Helpers for selecting overrides static bool areOverrideInputsConsistent( A_SyncOverride const& ov ) noexcept; static bool wouldInputsNeedAdjustment( A_SyncOverride const& ov ) noexcept; bool areOverrideInputsDisplayed( A_SyncOverride const& ov ) const noexcept; void overrideTrackToggled( A_SyncOverride const& ov , bool selected ) noexcept; // Selection display/edition windows void displayTrackWindow( ) noexcept; void displaySegmentWindow( ) noexcept; void displayPointWindow( ) noexcept; //---------------------------------------------------------------------- // Colors, sizes, etc. const uint32_t ColFrame{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .8 } ) }; const uint32_t ColHeader{ ImGui::GetColorU32( ImVec4{ .5 , .5 , .5 , .8 } ) }; const uint32_t ColHeaderText{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , 1 } ) }; const uint32_t ColMain{ ImGui::GetColorU32( ImVec4{ .4 , .4 , .4 , .8 } ) }; const uint32_t ColSelection{ ImGui::GetColorU32( ImVec4{ .8 , 1 , .8 , .2 } ) }; const uint32_t ColPointNormal{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .25 } ) }; const uint32_t ColPointHovered{ ImGui::GetColorU32( ImVec4{ 1 , 1 , 1 , .75 } ) }; const uint32_t ColPointSelected{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .75 } ) }; const ImVec2 BtSize{ 20 , 0 }; // Sequencer settings float zoomLevel{ 0.f }; float startPos{ 0.f }; bool followTime{ true }; bool showLabels{ true }; // Misc stuff T_StringBuilder stringBuffer; // XXX damn this shit to fucking hell E_DNDType dnd{ E_DNDType::NONE }; // Computed metrics bool checkLockMode{ false }; float vScroll{ 0 }; float barWidth; float cursorPos; uint32_t startBar; float startBarPos; float timePerBar; float totalPixels; float startPixel; // Track display T_Array< T_TrackDisplay > dspTracks; T_Array< T_TrackSegDisplay > dspSegments; T_Array< T_TrackPointDisplay > dspPoints; // Zoom area selection float firstZoomPixel; float curZoomPixel; // Selected item T_Optional< T_SyncTrackId > selId{ }; T_Optional< uint32_t > selSegment; T_Optional< uint32_t > selPoint; float selPointDnDStart , selPointDnDCur; // Original and copy of curve being modified E_ChangeType selUpdate{ E_ChangeType::NONE }; T_TempCurveStorage_ selUpdatingOriginals; T_TempCurveStorage_ selUpdatingCopies; // Override edition and copypasta T_PointData_ selEditor{ selId , selSegment , selPoint , selUpdatingOriginals , selUpdatingCopies }; T_String cpType; T_AutoArray< float , 16 > cpData; // Sub-windows E_SubWindow sub{ SW_NONE }; E_SubWindow tsSelectedTab{ SW_OVERRIDE_SELECTOR }; // Track selection T_KeyValueTable< T_String , bool > sInputs; T_Set< T_SyncTrackId > sTracks; T_String curveFinder; }; constexpr float T_SyncViewImpl_::BarWidth; /*----------------------------------------------------------------------------*/ bool T_SyncViewImpl_::display( ) noexcept { using namespace ImGui; auto const& dspSize( GetIO( ).DisplaySize ); // Window set-up SetNextWindowSize( ImVec2( dspSize.x , dspSize.y * .34f ) , ImGuiSetCond_Appearing ); SetNextWindowPos( ImVec2( 0 , dspSize.y * .66f ) , ImGuiSetCond_Appearing ); bool displayed{ true }; Begin( "Sequencer" , &displayed , ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollWithMouse ); if ( !displayed ) { End( ); return false; } checkTracks( ); checkSelection( ); displayToolbar( ); // Sequencer widget PushItemWidth( -1 ); sequencerWidget( ); PopItemWidth( ); End( ); // Close sub-window if e.g. selection has changed switch( sub ) { case SW_POINT: if ( !selPoint ) { sub = SW_SEGMENT; } // fallthrough case SW_SEGMENT: if ( !selSegment ) { sub = SW_TRACK; } // fallthrough case SW_TRACK: if ( !selId ) { sub = SW_NONE; } break; default: break; } // Display selected sub-window switch ( sub ) { case SW_NONE: break; case SW_INPUT_SELECTOR: case SW_OVERRIDE_SELECTOR: displayTrackSelectorWindow( ); break; case SW_TRACK: displayTrackWindow( ); break; case SW_SEGMENT: displaySegmentWindow( ); break; case SW_POINT: displayPointWindow( ); break; } return true; } void T_SyncViewImpl_::checkTracks( ) noexcept { auto& sync{ Common::Sync( ) }; // Check for overrides that have gone missing or that have become // inconsistent. { bool ovRemoved{ false }; for ( auto i = 0u ; i < sTracks.size( ) ; ) { const auto& t{ sTracks[ i ] }; if ( !t.isOverride ) { i ++; continue; } A_SyncOverride const* const ovr{ sync.getOverride( t.id ) }; if ( ovr && areOverrideInputsConsistent( *ovr ) ) { i++; continue; } sTracks.remove( t ); ovRemoved = true; } if ( !ovRemoved ) { return; } } // Remove all curves that come from overrides for ( auto i = 0u ; i < sInputs.size( ) ; ) { if ( sInputs.values( )[ i ] ) { sInputs.remove( sInputs.keys( )[ i ] ); } else { i ++; } } // Re-add curves for the remaining overrides const auto no{ sTracks.size( ) }; for ( auto i = 0u ; i < no ; i ++ ) { auto const& t{ sTracks[ i ] }; auto const* const od{ sync.getOverride( t.id ) }; assert( od ); const auto ni{ od->inputNames( ).size( ) }; for ( auto j = 0u ; j < ni ; j ++ ) { const bool ok{ sInputs.add( od->inputNames( )[ j ] , true ) }; assert( ok ); (void) ok; } } } void T_SyncViewImpl_::checkSelection( ) noexcept { if ( !selId ) { return; } auto& sync{ Common::Sync( ) }; auto const* const curve{ sync.getCurve( selId->isOverride ? sync.getOverride( selId->id )->inputNames( )[ 0 ] : selId->id ) }; // Missing curve if ( !( curve || selId->isOverride ) ) { // Remove segment/point selection if ( selSegment ) { selSegment.clear( ); selPoint.clear( ); } // If there's no matching input, unselect the track if ( !sync.hasInput( selId->id ) ) { selId.clear( ); } } else { // No segment selected? We're ok. if ( !selSegment ) { return; } // Check segment and point if ( *selSegment >= curve->segments.size( ) ) { selSegment.clear( ); selPoint.clear( ); } else if ( selPoint && *selPoint >= curve->segments[ *selSegment ].values.size( ) ) { selPoint.clear( ); } else { return; } } // If we were doing something with the curve, get rid of that too if ( selUpdate != E_ChangeType::NONE ) { selUpdate = E_ChangeType::NONE; selUpdatingCopies.clear( ); selUpdatingOriginals.clear( ); } } void T_SyncViewImpl_::displayToolbar( ) noexcept { using namespace ImGui; auto& sync{ Common::Sync( ) }; UI::Main( ).actionButton( sync.playing( ) ? "Stop" : "Play" ); SameLine( ); if ( ToolbarButton( ICON_FA_BACKWARD , BtSize , "Rewind to 00:00.000" ) ) { sync.setTime( 0 ); } SameLine( ); { char tBuffer[ 12 ]; TimeToString_( tBuffer , sizeof( tBuffer ) , sync.time( ) ); Text( "%s" , tBuffer ); } SameLine( ); if ( ToolbarButton( ICON_FA_CLOCK_O , BtSize , "Change duration and time units." ) ) { UI::Main( ).pushDialog( NewOwned< T_ChangeDurationDialog_ >( sync.durationUnits( ) , sync.durationUnitSize( ) ) ); } ToolbarSeparator( ); Text( ICON_FA_SEARCH ); bool zoomHovered{ IsItemHovered( ) }; SameLine( ); PushItemWidth( 100 ); SliderFloat( "##zoom" , &zoomLevel , 0 , 1 , "%.2f" ); if ( zoomHovered || IsItemHovered( ) ) { BeginTooltip( ); Text( "Zoom level" ); EndTooltip( ); } PopItemWidth( ); SameLine( ); if ( ToolbarButton( followTime ? ICON_FA_LOCK : ICON_FA_UNLOCK , BtSize , followTime ? "Follows the current position.\nClick to untie." : "Not tied to the current position.\nClick to follow." , true , followTime ) ) { followTime = !followTime; } ToolbarSeparator( ); if ( ToolbarButton( ICON_FA_TAGS , BtSize , "Toggle track labels." , true , showLabels ) ) { showLabels = !showLabels; } SameLine( ); if ( ToolbarButton( ICON_FA_LIST , BtSize , "Select inputs or sets thereof to display & edit." ) ) { const bool displaySelector{ sub == SW_INPUT_SELECTOR || sub == SW_OVERRIDE_SELECTOR }; sub = displaySelector ? SW_NONE : tsSelectedTab; curveFinder = T_String{}; } SameLine( ); if ( ToolbarButton( ICON_FA_BAN , BtSize , "Clear selected tracks." , sTracks.size( ) != 0 ) ) { clearSelectedTracks( ); } SameLine( ); if ( ToolbarButton( ICON_FA_COG , BtSize , "Select tracks automatically." ) ) { autoselectTracks( ); } ToolbarSeparator( ); if ( ToolbarButton( ICON_FA_MINUS , BtSize , "Show track info." , selId && sub != SW_TRACK ) ) { sub = SW_TRACK; } SameLine( ); if ( ToolbarButton( ICON_FA_ARROWS_H , BtSize , "Show segment info." , selId && selSegment && sub != SW_SEGMENT ) ) { sub = SW_SEGMENT; } SameLine( ); if ( ToolbarButton( ICON_FA_CIRCLE , BtSize , "Show point info." , selId && selSegment && selPoint && sub != SW_POINT ) ) { sub = SW_POINT; } } /*------------------------------------------------------------------------------*/ void T_SyncViewImpl_::sequencerWidget( ) noexcept { using namespace ImGui; const auto width{ CalcItemWidth( ) }; auto* const win{ GetCurrentWindow( ) }; const auto seqId{ win->GetID( "##sequencer" ) }; const ImVec2 cPos{ win->DC.CursorPos }; const ImVec2 ws{ GetWindowContentRegionMax( ) }; auto& style( ImGui::GetStyle( ) ); const float widgetWidth{ width - style.ScrollbarSize }; const ImRect bbHeader{ cPos , cPos + ImVec2( widgetWidth , SeqHeaderHeight ) }; const ImRect bbDisplay{ ImVec2{ cPos.x , bbHeader.Max.y } , ImVec2{ cPos.x + widgetWidth , GetWindowPos( ).y + ws.y - style.FramePadding.y * 2 - style.ScrollbarSize } }; const ImRect bbAll{ bbHeader.Min , bbDisplay.Max }; ItemSize( bbAll , style.FramePadding.y ); if ( !ItemAdd( bbAll , seqId ) ) { return; } const bool hovered{ ItemHoverable( bbAll , seqId ) }; computeMetrics( std::max( 0.f , widgetWidth - 2.f ) ); BeginGroup( ); const auto hdrId{ win->GetID( "##header" ) }; const auto dspId{ win->GetID( "##display" ) }; PushID( seqId ); if ( ItemAdd( bbHeader , hdrId ) ) { PushID( hdrId ); sequencerHeader( bbHeader ); PopID( ); } if ( bbDisplay.Min.y < bbDisplay.Max.y && ItemAdd( bbDisplay , dspId ) ) { PushID( dspId ); sequencerBody( bbDisplay ); PopID( ); } // Vertical scrollbar - tracks const float thMul{ ( showLabels ? ( TrackLabelHeight + TrackPadding ) : 0 ) + TrackHeight + TrackPadding * 2.f }; const float totalHeight{ sTracks.size( ) * thMul + TrackPadding }; if ( vScroll > totalHeight - bbDisplay.GetHeight( ) ) { vScroll = ImMax( 0.f , totalHeight - bbDisplay.GetHeight( ) ); } UserScrollbar( false , totalHeight , bbDisplay.GetHeight( ) , &vScroll , bbHeader.GetTR( ) , bbAll.GetHeight( ) ); // Horizontal scrollbar - time auto& sync( Common::Sync( ) ); float rsPos = startPixel; if ( UserScrollbar( true , totalPixels , widgetWidth , &rsPos , bbDisplay.GetBL( ) , bbDisplay.GetWidth( ) ) ) { startPos = sync.durationUnits( ) * rsPos / totalPixels; checkLockMode = true; } PopID( ); EndGroup( ); auto& io( GetIO( ) ); if ( hovered && ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) { SetActiveID( seqId , win ); FocusWindow( win ); } const bool active( GetCurrentContext( )->ActiveId == seqId ); const float mPixels{ io.MousePos.x - bbAll.Min.x + startPixel }; const float mTime{ mPixels * sync.duration( ) / totalPixels }; if ( !active ) { if ( !hovered ) { return; } if ( io.MouseWheel != 0 ) { if ( io.KeyShift ) { zoomLevel = ImSaturate( zoomLevel + .025 * io.MouseWheel ); } else { vScroll = ImClamp( vScroll - 3.f * io.MouseWheel , 0.f , ImMax( 0.f , totalHeight - bbDisplay.GetHeight( ) ) ); } } if ( bbDisplay.Contains( io.MousePos ) ) { displayTooltips( mTime ); } if ( ImGuiIO_MouseHorizWheel != 0 ) { startPos = ImClamp( sync.durationUnits( ) * ( startPixel - 20.f * ImGuiIO_MouseHorizWheel ) / totalPixels , 0.f , sync.durationUnits( ) ); checkLockMode = true; } return; } if ( dnd == E_DNDType::ZOOM ) { if ( io.MouseDown[ 1 ] ) { curZoomPixel = mPixels; return; } checkLockMode = true; const auto zMin{ std::min( firstZoomPixel , curZoomPixel ) } , zMax{ std::max( firstZoomPixel , curZoomPixel ) } , diff{ zMax - zMin }; if ( diff > 4 ) { const float u( sync.durationUnits( ) ); startPos = ImClamp( zMin * u / totalPixels , 0.f , u ); if ( ( width - 2.f ) / u >= BarWidth ) { zoomLevel = 0; } else { const auto length{ std::min( u , diff * u / totalPixels ) }; const auto ppu{ std::min( ( width - 2 ) / length , BarWidth ) }; zoomLevel = ( ppu - BarWidth ) / ( BarWidth - width / u ) + 1; } } } else if ( dnd == E_DNDType::POINT && handlePointDrag( mPixels , io.MouseDown[ 0 ] , io.KeyShift ) ) { return; } else if ( dnd == E_DNDType::CLICK && ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) { return; } else if ( dnd == E_DNDType::TIME && io.MouseDown[ 0 ] ) { sync.setTime( mTime ); return; } if ( dnd != E_DNDType::NONE && !( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) { dnd = E_DNDType::NONE; ClearActiveID( ); return; } const auto mp{ getMousePos( ) }; if ( mp.type == E_MousePosType::NONE ) { if ( io.MouseDown[ 0 ] ) { sync.setTime( mTime ); dnd = E_DNDType::TIME; } else if ( io.MouseDown[ 1 ] ) { firstZoomPixel = mPixels; dnd = E_DNDType::ZOOM; curZoomPixel = mPixels; } } else if ( mp.type == E_MousePosType::TRACK ) { auto const& dTrack{ dspTracks[ mp.index ] }; if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) { selId = dTrack.id; selSegment = decltype( selSegment ){}; selPoint = decltype( selPoint ){}; sub = E_SubWindow::SW_TRACK; dnd = E_DNDType::CLICK; } } else if ( mp.type == E_MousePosType::SEGMENT ) { auto const& dSeg{ dspSegments[ mp.index ] }; auto const& dTrack{ dspTracks[ dSeg.track ] }; if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) { selId = dTrack.id; selSegment = dSeg.seg; selPoint = decltype( selPoint ){}; sub = E_SubWindow::SW_SEGMENT; dnd = E_DNDType::CLICK; } } else if ( mp.type == E_MousePosType::POINT ) { auto const& dPoint{ dspPoints[ mp.index ] }; auto const& dSeg{ dspSegments[ dPoint.seg ] }; auto const& dTrack{ dspTracks[ dSeg.track ] }; if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) { selId = dTrack.id; selSegment = dSeg.seg; selPoint = dPoint.index; const bool pdnd{ io.MouseDown[ 0 ] && ( dPoint.index != 0 || dSeg.seg != 0 ) }; if ( pdnd ) { assert( selUpdate == E_ChangeType::NONE ); selPointDnDStart = selPointDnDCur = mPixels; if ( selId->isOverride ) { auto const& names{ sync.getOverride( selId->id )->inputNames( ) }; const auto ni{ names.size( ) }; for ( auto i = 0u ; i < ni ; i ++ ) { selUpdatingOriginals.add( *sync.getCurve( names[ i ] ) ); } } else { selUpdatingOriginals.add( *sync.getCurve( selId->id ) ); } selUpdate = E_ChangeType::POINT_DND; dnd = E_DNDType::POINT; } else { dnd = E_DNDType::CLICK; } sub = E_SubWindow::SW_POINT; } } } void T_SyncViewImpl_::computeMetrics( const float innerWidth ) noexcept { auto& sync( Common::Sync( ) ); const uint32_t units{ sync.durationUnits( ) }; zoomLevel = ImSaturate( zoomLevel ); const float zoom1Pixels{ std::max( units * BarWidth , innerWidth ) }; totalPixels = zoomLevel * ( zoom1Pixels - innerWidth ) + innerWidth; const uint32_t totalBars{ [=](){ const float b{ std::max( std::min( totalPixels / BarWidth , float( units ) ) , 1.f ) }; const float mod{ fmod( b , 1.f ) }; return uint32_t( b + ( mod ? ( 1 - mod ) : 0 ) ); }() }; const float unitsPerBar{ float( units ) / totalBars }; barWidth = totalPixels / totalBars; const float absCursorPos{ sync.time( ) * totalPixels / sync.duration( ) }; if ( followTime ) { const float dispUnits{ innerWidth * units / totalPixels }; const float uSize{ sync.durationUnitSize( ) }; const float endPos{ std::min( startPos + dispUnits , float( units ) ) }; startPos = endPos - dispUnits; const float spp{ startPos * totalPixels / units }; const float epp{ endPos * totalPixels / units }; if ( absCursorPos < spp || absCursorPos > epp ) { if ( checkLockMode ) { followTime = false; } else { startPos = std::max( 0.f , sync.time( ) / uSize - unitsPerBar * .5f ); } } } const float unadjustedStartPixel{ totalPixels * startPos / units }; if ( unadjustedStartPixel + innerWidth > totalPixels ) { startPos = std::max( 0.f , units * ( totalPixels - innerWidth ) / totalPixels ); } startPixel = totalPixels * startPos / units; startBar = [=](){ const float b{ startPixel * totalBars / totalPixels }; const float mod{ fmod( b , 1.f ) }; return uint32_t( std::max( 0 , int32_t( b - ( mod ? mod : 1 ) ) ) ); }(); startBarPos = startBar * barWidth - startPixel; cursorPos = absCursorPos - startPixel; timePerBar = unitsPerBar * sync.durationUnitSize( ); assert( startBarPos <= 0 ); assert( totalPixels >= innerWidth ); checkLockMode = false; } void T_SyncViewImpl_::sequencerHeader( ImRect const& bb ) noexcept { using namespace ImGui; auto* const dl( GetWindowDrawList( ) ); const ImRect inner{ bb.Min + ImVec2{ 1 , 1 } , bb.Max - ImVec2{ 1 , 1 } }; dl->AddRectFilled( inner.Min , inner.Max , ColHeader ); dl->AddRect( bb.Min , bb.Max , ColFrame ); if ( cursorPos >= 0 && cursorPos <= inner.GetWidth( ) ) { auto* const dl( GetWindowDrawList( ) ); dl->AddLine( inner.Min + ImVec2{ cursorPos , 0 } , ImVec2{ inner.Min.x + cursorPos , inner.Max.y - 1 } , GetColorU32( ImVec4{ 1 , 1 , 1 , .5 } ) ); } PushFont( UI::Main( ).smallFont( ) ); PushStyleColor( ImGuiCol_Text , ColHeaderText ); auto pos{ startBarPos }; auto bar{ startBar }; const auto max{ bb.GetWidth( ) + barWidth - 2.f }; char buffer[ 12 ]; while ( pos < max ) { const ImVec2 taStart{ inner.Min + ImVec2{ pos - barWidth * .5f , 0 } }; const ImVec2 taEnd{ taStart + ImVec2{ barWidth , inner.Max.y - inner.Min.y } }; const float time{ bar * timePerBar }; TimeToString_( buffer , sizeof( buffer ) , time ); RenderTextClipped( taStart , taEnd , buffer , nullptr , nullptr , ImVec2{ .5f , .05f + ( ( bar % 2 ) ? .9f : 0.f ) } , &inner ); pos += barWidth; bar ++; } PopStyleColor( ); PopFont( ); } void T_SyncViewImpl_::sequencerBody( ImRect const& bb ) noexcept { using namespace ImGui; auto* const dl( GetWindowDrawList( ) ); const ImRect inner{ bb.Min + ImVec2{ 1 , 1 } , bb.Max - ImVec2{ 1 , 1 } }; dl->AddRectFilled( inner.Min , inner.Max , ColMain ); dl->AddRect( bb.Min , bb.Max , ColFrame ); if ( dnd == E_DNDType::ZOOM ) { const float z0{ ImClamp( firstZoomPixel - startPixel + inner.Min.x , inner.Min.x , inner.Max.x ) } , z1{ ImClamp( curZoomPixel - startPixel + bb.Min.x , inner.Min.x , inner.Max.x ) }; const float zMin{ std::min( z0 , z1 ) } , zMax{ std::max( z0 , z1 ) }; if ( zMin != zMax ) { dl->AddRectFilled( ImVec2{ zMin , inner.Min.y } , ImVec2{ zMax , inner.Max.y } , ColSelection ); } } auto pos{ startBarPos }; auto bar{ startBar }; const auto max{ bb.GetWidth( ) + barWidth - 2.f }; while ( pos < max ) { if ( pos >= 0 && pos < inner.GetWidth( ) ) { dl->AddLine( bb.Min + ImVec2{ pos , 0 } , ImVec2{ inner.Min.x + pos , inner.Max.y } , 0xff000000 ); } pos += barWidth; bar ++; } // Display the curve / override controls dspTracks.clear( ); dspSegments.clear( ); dspPoints.clear( ); if ( sTracks.size( ) != 0 ) { sequencerTracks( inner ); } if ( cursorPos >= 0 && cursorPos <= inner.GetWidth( ) ) { auto* const dl( GetWindowDrawList( ) ); dl->AddLine( inner.Min + ImVec2{ cursorPos , -1 } , ImVec2{ inner.Min.x + cursorPos , inner.Max.y - 1 } , 0xffffffff ); } } void T_SyncViewImpl_::sequencerTracks( ImRect const& container ) noexcept { ImRect bb{ container }; bb.Min.y += TrackPadding - vScroll; bb.Max.y = bb.Min.y + TrackHeight; float hue{ 0.12f }; auto& sync{ Common::Sync( ) }; const auto nc{ sTracks.size( ) }; for ( auto i = 0u ; i < nc ; i ++ ) { auto const& id{ sTracks[ i ] }; auto* const curve{ sync.getCurve( sTracks[ i ].isOverride ? sync.getOverride( id.id )->inputNames( )[ 0 ] : id.id ) }; if ( showLabels ) { using namespace ImGui; stringBuffer.clear( ); if ( id.isOverride ) { stringBuffer << "[O] " << sync.getOverride( id.id )->fullTitle( ); } else { stringBuffer << "[I] " << id.id; } PushStyleColor( ImGuiCol_Text , ColorHSVAToU32( hue , .05f , 1.f , 1.f ) ); char const* const tStart{ stringBuffer.data( ) }; char const* const taEnd{ tStart + stringBuffer.size( ) }; const ImVec2 ts{ CalcTextSize( tStart , taEnd ) }; RenderTextClipped( bb.Min + ImVec2{ 20 , 0 } , bb.Min + ImVec2{ 20 + ts.x , TrackLabelHeight } , tStart , taEnd , &ts , ImVec2{ 0.f , .5f } , &container ); PopStyleColor( ); bb.Min.y += TrackLabelHeight + TrackPadding; bb.Max.y += TrackLabelHeight + TrackPadding; } sequencerTrack( hue , bb , container , id , curve ); bb.Min.y += TrackHeight + 2 * TrackPadding; bb.Max.y += TrackHeight + 2 * TrackPadding; hue = fmodf( hue + .17f , 1.f ); } } void T_SyncViewImpl_::sequencerTrack( float& hue , ImRect const& bb , ImRect const& container , T_SyncTrackId const& id , T_SyncCurve const* curve ) noexcept { // Don't display if the track is fully hidden if ( !container.Overlaps( bb ) ) { return; } const ImRect bar{ ImVec2{ ImFloor( bb.Min.x ) , ImFloor( ImMax( container.Min.y , bb.Min.y ) ) } , ImVec2{ ImFloor( bb.Max.x ) - 1 , ImFloor( ImMin( container.Max.y , bb.Max.y ) ) } }; // Add track display record const auto dTrackIdx{ dspTracks.size( ) }; auto& dTrack{ dspTracks.addNew( ) }; dTrack.id = id; dTrack.dispSegs = 0; dTrack.firstSeg = dspSegments.size( ); dTrack.area = bb; // Compute colors using namespace ImGui; auto const& mp{ GetIO( ).MousePos }; const bool sCurve{ selId && id == *selId }; const bool barHovered{ bar.Contains( mp ) }; const float scv{ sCurve ? 1.f : ( barHovered ? .85f : .7f ) }; const auto bgColor{ ColorHSVAToU32( hue , .25f , scv , .25f ) } , borderColor{ ColorHSVAToU32( hue , .5f , scv , 1.f ) }; const uint32_t segColors[] = { ColorHSVAToU32( hue - .03f , .4f , .7f , 1.f ) , ColorHSVAToU32( hue + .03f , .4f , .7f , 1.f ) , ColorHSVAToU32( hue - .03f , .4f , .85f , 1.f ) , ColorHSVAToU32( hue + .03f , .4f , .85f , 1.f ) , ColorHSVAToU32( hue - .03f , .4f , 1.f , 1.f ) , ColorHSVAToU32( hue + .03f , .4f , 1.f , 1.f ) , }; // Draw the bar itself auto* const dl{ GetWindowDrawList( ) }; dl->AddRectFilled( bar.Min , bar.Max , bgColor ); if ( container.Contains( bb.GetTL( ) ) ) { dl->AddLine( bar.GetTL( ) , bar.GetTR( ) , borderColor ); } if ( container.Contains( bb.GetBL( ) ) ) { dl->AddLine( bar.GetBL( ) , bar.GetBR( ) , borderColor ); } // Left only has a border if this is the start if ( startPos == 0.f ) { dl->AddLine( bar.GetTL( ) , bar.GetBL( ) , borderColor ); } // Right has a border if the end is being displayed. if ( startPixel + container.GetWidth( ) >= totalPixels ) { dl->AddLine( bar.GetTR( ) , bar.GetBR( ) , borderColor ); } dl->PushClipRect( bar.Min , bar.Max ); // If there's a curve, go through all segments const auto units{ Common::Sync( ).durationUnits( ) }; const bool useCopy{ sCurve && curve && !selUpdatingCopies.empty( ) }; const auto nSeg{ curve ? ( useCopy ? selUpdatingCopies[ 0 ] : *curve ).segments.size( ) : 0u }; uint32_t segStart{ 0 }; for ( auto i = 0u ; i < nSeg ; i ++ ) { auto const& seg{ ( useCopy ? selUpdatingCopies[ 0 ] : *curve ).segments[ i ] }; const auto segDur{ [&](){ auto t{ 0u }; for ( auto d : seg.durations ) { t += d; } return t; }() }; const float xStart{ ImFloor( container.Min.x + ( segStart - startPos ) * totalPixels / units ) }; const float xEnd{ xStart + ImFloor( segDur * totalPixels / units ) }; const ImRect segFull{ ImVec2{ xStart , bar.Min.y + 1 } , ImVec2{ xEnd , bar.Max.y } }; segStart += segDur; if ( !segFull.Overlaps( bar ) ) { continue; } // Add segment to displayed list const bool sSegment{ sCurve && selSegment && *selSegment == i }; const bool segHovered{ segFull.Contains( mp ) }; const auto color{ segColors[ i % 2 + ( sSegment ? 4 : ( segHovered ? 2 : 0 ) ) ] }; auto dSegIdx{ dspSegments.size( ) }; auto& dSeg{ dspSegments.addNew( ) }; dSeg.area = segFull; dSeg.track = dTrackIdx; dSeg.seg = i; dSeg.dispPoints = 0; dSeg.firstPoint = dspPoints.size( ); dTrack.dispSegs ++; // Draw segment const ImRect segBar{ ImVec2{ ImMax( bar.Min.x , segFull.Min.x ) , segFull.Min.y } , ImVec2{ ImMin( bar.Max.x , segFull.Max.x ) , segFull.Max.y } }; dl->AddRectFilled( segBar.Min , segBar.Max , color ); dl->PushClipRect( segBar.Min , segBar.Max ); // Handle points const auto nd{ seg.durations.size( ) }; const auto ym{ bb.Min.y + PointRadius + 1 }; auto cDur{ 0 }; for ( auto j = 0u ; j < nd + 1 ; j ++ ) { const ImVec2 ctr{ std::roundf( xStart + cDur * totalPixels / units ) , ym }; const bool sPoint{ sSegment && selPoint && *selPoint == j }; const bool hpt{ !sPoint && segHovered && ImLengthSqr( mp - ctr ) <= 2.25 * PointRadiusSqr }; dl->AddCircleFilled( ctr , PointRadius , sPoint ? ColPointSelected : ( hpt ? ColPointHovered : ColPointNormal ) ); cDur += j < nd ? seg.durations[ j ] : 0; // Add point record auto& dPoint{ dspPoints.addNew( ) }; dPoint.center = ctr; dPoint.index = j; dPoint.mode = j == 0 ? T_TrackPointDisplay::START : ( j == nd ? T_TrackPointDisplay::END : T_TrackPointDisplay::MIDDLE ); dPoint.seg = dSegIdx; dSeg.dispPoints ++; } dl->PopClipRect( ); } dl->PopClipRect( ); } /*----------------------------------------------------------------------------*/ void T_SyncViewImpl_::displayTooltips( const float time ) noexcept { auto mp{ getMousePos( ) }; if ( mp.type == E_MousePosType::NONE ) { return; } // Track / segment / point from mouse info auto const& track( [&](){ if ( mp.type == E_MousePosType::POINT ) { return dspTracks[ dspSegments[ dspPoints[ mp.index ].seg ].track ]; } if ( mp.type == E_MousePosType::SEGMENT ) { return dspTracks[ dspSegments[ mp.index ].track ]; } return dspTracks[ mp.index ]; }() ); auto const* const seg( [&]() -> T_TrackSegDisplay const* { if ( mp.type == E_MousePosType::TRACK ) { return nullptr; } if ( mp.type == E_MousePosType::SEGMENT ) { return &dspSegments[ mp.index ]; } return &dspSegments[ dspPoints[ mp.index ].seg ]; }() ); auto const* const point( [&]() -> T_TrackPointDisplay const* { if ( mp.type != E_MousePosType::POINT ) { return nullptr; } return &dspPoints[ mp.index ]; }() ); // Curve from track auto& sync{ Common::Sync( ) }; auto const* const ovr{ track.id.isOverride ? sync.getOverride( track.id.id ) : nullptr }; auto const* const curve{ sync.getCurve( track.id.isOverride ? ovr->inputNames( )[ 0 ] : track.id.id ) }; assert( mp.type == E_MousePosType::TRACK || curve != nullptr ); // Time offset const float dTime( [&](){ if ( point ) { auto tDur{ .0f }; for ( auto i = 0u ; i <= seg->seg ; i ++ ) { auto const& s{ curve->segments[ i ] }; auto const nd{ i == seg->seg ? point->index : s.durations.size( ) }; for ( auto j = 0u ; j < nd ; j ++ ) { tDur += s.durations[ j ]; } } return tDur * sync.durationUnitSize( ); } return time; }() ); const float dUTime{ dTime / sync.durationUnitSize( ) }; char buffer[ 12 ]; TimeToString_( buffer , sizeof( buffer ) , dTime ); stringBuffer.clear( ) << ( track.id.isOverride ? ovr->title( ) : track.id.id ) << ' ' << ( track.id.isOverride ? "(override)" : "(input)" ) << '\n'; if ( mp.type == E_MousePosType::TRACK ) { stringBuffer << "No segment"; } else if ( ovr ) { if ( mp.type == E_MousePosType::SEGMENT ) { int cType{ -1 }; for ( auto i = 0u ; i < ovr->inputNames( ).size( ) ; i ++ ) { auto const& iName{ ovr->inputNames( )[ i ] }; auto const* const c{ sync.getCurve( iName ) }; if ( i == 0 ) { cType = c->segments[ seg->seg ].type; } else if ( cType != c->segments[ seg->seg ].type ) { cType = -1; } } if ( cType == -1 ) { stringBuffer << "Various segment types"; } else { stringBuffer << "Segment type: " << T_SyncSegment::E_SegmentType( cType ); } } else { stringBuffer << "Point index " << point->index; } } else { auto const& s{ curve->segments[ seg->seg ] }; if ( mp.type == E_MousePosType::SEGMENT ) { stringBuffer << "Segment type: " << s.type; } else { stringBuffer << "On " << s.type << " segment, index " << point->index; } } stringBuffer << "\nTime: " << buffer << '\n'; if ( track.id.isOverride ) { stringBuffer << "\nInput values:"; for ( auto i = 0u ; i < ovr->inputNames( ).size( ) ; i ++ ) { auto const& iName{ ovr->inputNames( )[ i ] }; auto const* const c{ sync.getCurve( iName ) }; const float value{ point ? c->segments[ seg->seg ].values[ point->index ] : ( c ? c->computeValue( dUTime ) : sync.inputs( )[ ovr->inputPositions( )[ i ] ] ) }; stringBuffer << "\n - " << iName << ": " << value; } } else { const float value{ point ? curve->segments[ seg->seg ].values[ point->index ] : ( curve ? curve->computeValue( dUTime ) : sync.inputs( )[ sync.inputPos( track.id.id ) ] ) }; stringBuffer << "Value: " << value; } using namespace ImGui; stringBuffer << '\0'; BeginTooltip( ); Text( "%s" , stringBuffer.data( ) ); EndTooltip( ); } bool T_SyncViewImpl_::handlePointDrag( const float mPixels , bool mouseDown , bool shiftPressed ) noexcept { auto& sync( Common::Sync( ) ); selPointDnDCur = mPixels; const float diff{ selPointDnDCur - selPointDnDStart }; const int32_t diffUnits{ int32_t( round( diff * sync.durationUnits( ) / totalPixels ) ) }; const bool moved{ fabsf( diff ) >= 2 && abs( diffUnits ) > 0 }; const auto ni{ selUpdatingCopies.size( ) }; const auto sid{ *selSegment }; const auto pid{ *selPoint }; assert( pid > 0 || sid > 0 ); const bool isLastPoint{ [&](){ auto const& s{ selUpdatingOriginals[ 0 ].segments }; return sid == s.size( ) - 1 && pid == s[ sid ].durations.size( ); }( ) }; if ( moved && ( shiftPressed || isLastPoint ) ) { // Increase/decrease duration selUpdatingCopies = selUpdatingOriginals; for ( auto i = 0u ; i < ni ; i ++ ) { auto& copy{ selUpdatingCopies[ i ] }; auto& seg{ copy.segments[ sid ] }; auto& d{ pid == 0 ? copy.segments[ sid - 1 ].durations.last( ) : seg.durations[ pid - 1 ] }; d = std::max( 1 , diffUnits + int32_t( d ) ); sync.setCurve( copy ); } } else if ( moved ) { // Move the point but keep the sum of before/after durations // a constant selUpdatingCopies = selUpdatingOriginals; for ( auto i = 0u ; i < ni ; i ++ ) { auto& copy{ selUpdatingCopies[ i ] }; auto& seg{ copy.segments[ sid ] }; auto& d0{ pid == 0 ? copy.segments[ sid - 1 ].durations.last( ) : seg.durations[ pid - 1 ] }; auto& d1{ pid == seg.durations.size( ) ? copy.segments[ sid + 1 ].durations[ 0 ] : seg.durations[ pid ] }; const int32_t mmNeg( 1 - d0 ) , mmPos( d1 - 1 ); const int32_t diff{ diffUnits < mmNeg ? mmNeg : ( diffUnits > mmPos ? mmPos : diffUnits ) }; d0 += diff; d1 -= diff; sync.setCurve( copy ); } } else { selUpdatingCopies.clear( ); for ( auto i = 0u ; i < ni ; i ++ ) { sync.setCurve( selUpdatingOriginals[ i ] ); } } if ( mouseDown ) { return true; } assert( selUpdate == E_ChangeType::POINT_DND ); selUpdate = E_ChangeType::NONE; if ( moved ) { auto& undo{ dynamic_cast< T_UndoSyncChanges& >( Common::Undo( ).add< T_UndoSyncChanges >( ) ) }; for ( auto i = 0u ; i < ni ; i ++ ) { undo.curveReplacement( std::move( selUpdatingOriginals[ i ] ) , std::move( selUpdatingCopies[ i ] ) ); } } else { for ( auto i = 0u ; i < ni ; i ++ ) { sync.setCurve( std::move( selUpdatingOriginals[ i ] ) ); } } selUpdatingOriginals.clear( ); selUpdatingCopies.clear( ); return false; } /*----------------------------------------------------------------------------*/ T_SyncViewImpl_::T_MousePos T_SyncViewImpl_::getMousePos( ) const noexcept { auto const& mp{ ImGui::GetIO( ).MousePos }; for ( auto i = 0u ; i < dspTracks.size( ) ; i ++ ) { auto const& track{ dspTracks[ i ] }; if ( !track.area.Contains( mp ) ) { continue; } for ( auto j = 0u ; j < track.dispSegs ; j ++ ) { auto const& seg{ dspSegments[ track.firstSeg + j ] }; if ( !seg.area.Contains( mp ) ) { continue; } for ( auto k = 0u ; k < seg.dispPoints ; k ++ ) { auto const& p{ dspPoints[ seg.firstPoint + k ] }; if ( ImLengthSqr( mp - p.center ) <= PointRadiusSqr * 2.25 ) { return T_MousePos{ E_MousePosType::POINT , seg.firstPoint + k }; } } return T_MousePos{ E_MousePosType::SEGMENT , track.firstSeg + j }; } return T_MousePos{ E_MousePosType::TRACK , i }; } return T_MousePos{ E_MousePosType::NONE , 0 }; } /*----------------------------------------------------------------------------*/ void T_SyncViewImpl_::displayTrackSelectorWindow( ) noexcept { using namespace ImGui; auto const& dspSize( GetIO( ).DisplaySize ); // Window set-up SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing ); SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing ); bool displayed{ true }; Begin( "Display curves" , &displayed , ImGuiWindowFlags_NoCollapse ); if ( !displayed ) { End( ); sub = SW_NONE; return; } // "Tabs" const ImVec2 ws( GetWindowContentRegionMax( ) ); auto const& style( GetStyle( ) ); const float innerWidth{ ws.x - 2 * style.FramePadding.x }; constexpr float nButtons{ 2.f }; const float buttonWidth{ std::max( 50.f , ( innerWidth - nButtons * style.FramePadding.x ) / nButtons ) }; if ( FakeTab_( "Overrides" , sub == SW_OVERRIDE_SELECTOR , buttonWidth ) ) { sub = tsSelectedTab = SW_OVERRIDE_SELECTOR; } SameLine( 0 ); if ( FakeTab_( "Individual inputs" , sub == SW_INPUT_SELECTOR , buttonWidth ) ) { sub = tsSelectedTab = SW_INPUT_SELECTOR; } // Content switch ( sub ) { case SW_INPUT_SELECTOR: displayInputSelector( ); break; case SW_OVERRIDE_SELECTOR: displayOverrideSelector( ); break; default: fprintf( stderr , "unexpected bullshit in sync view\n" ); std::abort( ); } End( ); } void T_SyncViewImpl_::displayInputSelector( ) noexcept { using namespace ImGui; T_Array< T_String > names{ Common::Sync( ).inputNames( ) }; // Search box; FIXME, this is utterly hacky stringBuffer.clear( ) << curveFinder; while ( stringBuffer.size( ) < 100 ) { stringBuffer << '\0'; } Text( ICON_FA_SEARCH ); SameLine( ); PushItemWidth( -1 ); if ( InputText( "##find" , const_cast< char* >( stringBuffer.data( ) ) , stringBuffer.size( ) ) ) { curveFinder = T_String{ stringBuffer.data( ) , uint32_t( strlen( stringBuffer.data( ) ) ) }; } PopItemWidth( ); if ( curveFinder ) { for ( auto i = 0u ; i < names.size( ) ; ) { auto const& n( names[ i ] ); if ( n.find( curveFinder ) == -1 ) { names.removeSwap( i ); } else { i ++; } } } names.sort( ); // The list ImGui::BeginChild( "content" ); for ( auto const& n : names ) { const bool present{ sInputs.contains( n ) }; const bool overriden{ present && *sInputs.get( n ) }; if ( overriden ) { PushDisabled( ); } bool select{ present }; stringBuffer.clear( ) << n << '\0'; if ( Checkbox( stringBuffer.data( ) , &select ) ) { const T_SyncTrackId id{ n , false }; if ( select ) { sTracks.add( id ); sInputs.add( n , false ); } else { sTracks.remove( id ); sInputs.remove( n ); } } if ( overriden ) { PopDisabled( ); } } EndChild( ); } void T_SyncViewImpl_::displayOverrideSelector( ) noexcept { using namespace ImGui; /* * An override can be selected directly if the inputs that are part of * it are consistent. * * If the inputs are not consistent, it will be necessary to reset * some of their curves; best option would be to try and be clever, but * a temporary measure (resetting all curves to match the structure of * the first longest curve) would work. */ BeginChild( "content" ); Common::Sync( ).visitOverrides( [&]( T_SyncOverrideVisitor::T_Element element , const bool exit ) { if ( element.hasType< T_SyncOverrideSection* >( ) ) { auto const& sos{ *element.value< T_SyncOverrideSection* >( ) }; if ( sos.title == "*root*" ) { return true; } if ( exit ) { TreePop( ); return true; } return TreeNodeEx( &sos.cTitle[ 0 ] , ImGuiTreeNodeFlags_DefaultOpen ); } if ( ! exit ) { return false; } auto const& ov{ *element.value< A_SyncOverride* >( ) }; auto const& id{ ov.id( ) }; const bool present{ sTracks.contains( T_SyncTrackId{ id , true } ) }; const bool hasCurves{ !present && areOverrideInputsDisplayed( ov ) }; const bool consistent{ areOverrideInputsConsistent( ov ) }; if ( hasCurves ) { PushDisabled( ); } if ( !consistent ) { PushStyleColor( ImGuiCol_Text , 0xff0000ff ); } bool selected{ present }; if ( Checkbox( ov.title( ) , &selected ) ) { overrideTrackToggled( ov , selected ); } if ( !consistent ) { PopStyleColor( ); } if ( hasCurves ) { PopDisabled( ); } return true; } ); EndChild( ); } bool T_SyncViewImpl_::areOverrideInputsConsistent( A_SyncOverride const& ov ) noexcept { /* A pair of inputs are consistent if * - one or both inputs do not have curve information attached; * - the durations and segment boundaries of the shortest curve * attached to the inputs match the boundaries and durations from the * other input's curve OR the last segment is shorter but its end * matches the location of one of the points in the corresponding * segment of the other curve. */ auto const& in{ ov.inputNames( ) }; for ( auto i = 0u ; i < in.size( ) - 1 ; i ++ ) { T_SyncCurve const* const c0{ Common::Sync( ).getCurve( in[ i ] ) }; if ( !c0 ) { continue; } for ( auto j = i + 1 ; j < in.size( ) ; j ++ ) { T_SyncCurve const* const c1{ Common::Sync( ).getCurve( in[ j ] ) }; if ( !c1 ) { continue; } const auto res{ c0->matches( *c1 ) }; if ( res == E_SyncCurveMatch::MISMATCH ) { return false; } } } return true; } bool T_SyncViewImpl_::wouldInputsNeedAdjustment( A_SyncOverride const& ov ) noexcept { auto& sync{ Common::Sync( ) }; auto const& in{ ov.inputNames( ) }; auto nNulls{ 0u }; for ( auto i = 0u ; i < in.size( ) - 1 ; i ++ ) { T_SyncCurve const* const c0{ sync.getCurve( in[ i ] ) }; if ( !c0 ) { nNulls ++; continue; } for ( auto j = i + 1 ; j < in.size( ) ; j ++ ) { T_SyncCurve const* const c1{ sync.getCurve( in[ j ] ) }; if ( !c1 || c0->matches( *c1 ) != E_SyncCurveMatch::IDENTICAL ) { return false; } } } if ( !sync.getCurve( in[ in.size( ) - 1 ] ) ) { nNulls ++; } return nNulls != 0 && nNulls != in.size( ); } bool T_SyncViewImpl_::areOverrideInputsDisplayed( A_SyncOverride const& ov ) const noexcept { auto const& in{ ov.inputNames( ) }; for ( auto i = 0u ; i < in.size( ) ; i ++ ) { if ( sInputs.contains( in[ i ] ) ) { return true; } } return false; } void T_SyncViewImpl_::overrideTrackToggled( A_SyncOverride const& ov , const bool selected ) noexcept { auto const& in{ ov.inputNames( ) }; const T_SyncTrackId id{ ov.id( ) , true }; // Handle de-selection if ( !selected ) { sTracks.remove( id ); for ( auto i = 0u ; i < in.size( ) ; i ++ ) { sInputs.remove( in[ i ] ); } return; } // If the override is not consistent, we need to make it so SyncEditor::MakeOverrideConsistent( ov.id( ) ); sTracks.add( id ); for ( auto i = 0u ; i < in.size( ) ; i ++ ) { sInputs.add( in[ i ] , true ); } } /*----------------------------------------------------------------------------*/ void T_SyncViewImpl_::clearSelectedTracks( ) noexcept { sTracks.clear( ); sInputs.clear( ); selId.clear( ); selSegment.clear( ); selPoint.clear( ); } void T_SyncViewImpl_::autoselectTracks( ) noexcept { clearSelectedTracks( ); auto& sync{ Common::Sync( ) }; sync.visitOverrides( [&]( T_SyncOverrideVisitor::T_Element element , const bool exit ) { if ( element.hasType< T_SyncOverrideSection* >( ) || !exit ) { return true; } auto const& ov{ *element.value< A_SyncOverride* >( ) }; if ( !areOverrideInputsConsistent( ov ) ) { return true; } if ( wouldInputsNeedAdjustment( ov ) ) { return true; } auto const& in{ ov.inputNames( ) }; sTracks.add( T_SyncTrackId{ ov.id( ) , true } ); for ( auto i = 0u ; i < in.size( ) ; i ++ ) { sInputs.add( in[ i ] , true ); } return true; } ); for ( auto const& n : sync.inputNames( ) ) { if ( !sInputs.contains( n ) ) { sTracks.add( T_SyncTrackId{ n , false } ); sInputs.add( n , false ); } } } /*----------------------------------------------------------------------------*/ void T_SyncViewImpl_::displayTrackWindow( ) noexcept { using namespace ImGui; auto const& dspSize( GetIO( ).DisplaySize ); // Window set-up SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing ); SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing ); bool displayed{ true }; Begin( "Selected track" , &displayed , ImGuiWindowFlags_NoCollapse ); if ( !displayed ) { End( ); sub = SW_NONE; return; } // Get the curve (or the first curve from the set) auto& sync{ Common::Sync( ) }; auto const* const ovr{ selId->isOverride ? sync.getOverride( selId->id ) : nullptr }; auto const* const curve{ sync.getCurve( selId->isOverride ? ovr->inputNames( )[ 0 ] : selId->id ) }; if ( selId->isOverride ) { Text( "Override:" ); SameLine( 110 ); Text( "%s" , ovr->title( ) ); for ( auto i = 0u ; i < ovr->inputNames( ).size( ) ; i ++ ) { Text( i == 0 ? "Curve(s):" : "" ); SameLine( 110 ); Text( "%s" , ovr->inputNames( )[ i ].toOSString( ).data( ) ); } } else { Text( "Curve:" ); SameLine( 110 ); Text( "%s" , selId->id.toOSString( ).data( ) ); if ( !sync.hasInput( selId->id ) ) { Text( " " ); SameLine( 110 ); Text( "No matching input" ); } } Separator( ); Text( "Segments:" ); SameLine( 110 ); Text( "%d" , curve ? curve->segments.size( ) : 0 ); Separator( ); // Compute total duration const uint32_t duration{ [&](){ uint32_t t{ 0 }; if ( !curve ) { return 0u; } for ( auto const& s : curve->segments ) { for ( auto const& d : s.durations ) { t += d; } } return t; }() }; const float tDuration{ duration * sync.durationUnitSize( ) }; Text( "Duration:" ); SameLine( 110 ); Text( "%d units" , duration ); Text( " " ); SameLine( 110 ); Text( "%.3f seconds" , tDuration ); const float dDuration{ sync.duration( ) }; if ( tDuration == 0.f ) { Text( " " ); SameLine( 110 ); Text( "Empty track" ); } else if ( tDuration == dDuration ) { Text( " " ); SameLine( 110 ); Text( "Covers the whole demo" ); } else if ( tDuration > dDuration ) { Text( " " ); SameLine( 110 ); Text( "Longer than the demo" ); } if ( ( curve && !curve->segments.empty( ) ) || tDuration < dDuration ) { Separator( ); } if ( curve && !curve->segments.empty( ) ) { Text( " " ); SameLine( 110 ); if ( Button( "Clear" , ImVec2{ -1 , 0 } ) ) { SyncEditor::DeleteCurve( *selId ); } } if ( tDuration < dDuration ) { Text( " " ); SameLine( 110 ); if ( Button( "Append segment" , ImVec2{ -1 , 0 } ) ) { const uint32_t ns{ std::max( 1u , ( sync.durationUnits( ) - duration ) / 2 ) }; SyncEditor::AppendSegment( *selId , ns ); } } End( ); } void T_SyncViewImpl_::displaySegmentWindow( ) noexcept { using namespace ImGui; auto const& dspSize( GetIO( ).DisplaySize ); // Window set-up SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing ); SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing ); bool displayed{ true }; Begin( "Selected segment" , &displayed , ImGuiWindowFlags_NoCollapse ); if ( !displayed ) { End( ); sub = SW_NONE; return; } const uint32_t sid{ *selSegment }; auto& sync{ Common::Sync( ) }; auto const* const ovr{ selId->isOverride ? sync.getOverride( selId->id ) : nullptr }; if ( selId->isOverride ) { Text( "Override:" ); SameLine( 110 ); Text( "%s" , ovr->title( ) ); } else { Text( "Curve:" ); SameLine( 110 ); Text( "%s" , selId->id.toOSString( ).data( ) ); } Text( "Index:" ); SameLine( 110 ); Text( "%d" , sid ); Separator( ); // Access curve and segment auto const* const curve{ sync.getCurve( selId->isOverride ? ovr->inputNames( )[ 0 ] : selId->id ) }; auto const& segment{ curve->segments[ sid ] }; // Find start and duration uint32_t start{ 0 } , duration{ 0 }; for ( auto i = 0u ; i <= sid ; i ++ ) { auto const& s{ curve->segments[ i ] }; auto& tgt{ i == sid ? duration : start }; for ( auto d : s.durations ) { tgt += d; } } const float tStart{ sync.durationUnitSize( ) * start } , tDuration{ sync.durationUnitSize( ) * duration }; Text( "Start:" ); SameLine( 110 ); Text( "%d units" , start ); Text( " " ); SameLine( 110 ); Text( "%.3f seconds" , tStart ); Text( "End:" ); SameLine( 110 ); Text( "%d units" , start + duration ); Text( " " ); SameLine( 110 ); Text( "%.3f seconds" , tStart + tDuration ); Text( "Duration:" ); SameLine( 110 ); Text( "%d units" , duration ); Text( " " ); SameLine( 110 ); Text( "%.3f seconds" , tDuration ); // Generate the combo box's data static constexpr T_SyncSegment::E_SegmentType types[] = { T_SyncSegment::LINEAR , T_SyncSegment::RAMP , T_SyncSegment::SMOOTH }; stringBuffer.clear( ) << "various types" << '\0'; const auto ts{ stringBuffer.size( ) }; for ( auto t : types ) { stringBuffer << t << '\0'; } stringBuffer << '\0'; // Display segment type selectors auto const nc{ ovr ? ovr->inputNames( ).size( ) : 1u }; int change; int sTypes[ nc + 1 ]; Separator( ); if ( !ovr ) { Text( "Type:" ); SameLine( 110 ); PushItemWidth( -1 ); sTypes[ 0 ] = int( segment.type ); change = Combo( "##type" , &sTypes[ 0 ] , stringBuffer.data( ) + ts ) ? 0 : -1; PopItemWidth( ); } else if ( TreeNode( "Segment types" ) ) { change = -1; const uint32_t sblen{ stringBuffer.size( ) }; PushItemWidth( -1 ); int common{ -1 }; for ( auto i = 0u ; i < nc ; i ++ ) { auto const& iName{ ovr->inputNames( )[ i ] }; sTypes[ i ] = int( sync.getCurve( iName )->segments[ sid ].type ); if ( i == 0 ) { common = sTypes[ i ]; } else if ( common != sTypes[ i ] ) { common = -1; } stringBuffer << iName << '\0'; Text( "%s" , stringBuffer.data( ) + sblen ); SameLine( 200 ); stringBuffer.truncate( sblen ) << "##type" << i << '\0'; if ( Combo( stringBuffer.data( ) + sblen , &sTypes[ i ] , stringBuffer.data( ) + ts ) ) { change = i; } stringBuffer.truncate( sblen ); } Separator( ); const auto cOffset{ common == -1 ? 0 : ts }; if ( common == -1 ) { common ++; } Text( "All inputs" ); SameLine( 200 ); if ( Combo( "##type-all" , &common , stringBuffer.data( ) + cOffset ) ) { sTypes[ nc ] = common - ( cOffset == 0 ? 1 : 0 ); change = nc; } PopItemWidth( ); TreePop( ); } else { change = -1; } Separator( ); Text( "Points:" ); SameLine( 110 ); Text( "%d" , segment.values.size( ) ); if ( !ovr ) { // Find min/max float sMin{ FLT_MAX } , sMax{ -FLT_MAX }; for ( auto v : segment.values ) { sMin = ImMin( sMin , v ); sMax = ImMax( sMax , v ); } Text( "Min. value:" ); SameLine( 110 ); Text( "%f" , sMin ); Text( "Max. value:" ); SameLine( 110 ); Text( "%f" , sMax ); } Separator( ); Text( " " ); SameLine( 110 ); if ( Button( "Delete segment" , ImVec2{ -1 , 0 } ) ) { if ( curve->segments.size( ) > 1 ) { selSegment = ( sid == 0 ? 0 : ( sid - 1 ) ); } else { selSegment = decltype( selSegment ){}; } SyncEditor::DeleteSegment( *selId , sid ); } if ( change >= 0 && change < int32_t( nc ) ) { SyncEditor::SetSegmentType( *( ovr ? sync.getCurve( ovr->inputNames( )[ change ] ) : curve ) , sid , T_SyncSegment::E_SegmentType( sTypes[ change ] ) ); } else if ( change == int32_t( nc ) && sTypes[ nc ] != -1 ) { assert( ovr ); SyncEditor::SetSegmentTypes( *ovr , sid , T_SyncSegment::E_SegmentType( sTypes[ nc ] ) ); } End( ); } void T_SyncViewImpl_::displayPointWindow( ) noexcept { using namespace ImGui; auto const& dspSize( GetIO( ).DisplaySize ); // Window set-up SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing ); SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing ); bool displayed{ true }; Begin( "Selected point" , &displayed , ImGuiWindowFlags_NoCollapse ); if ( !displayed ) { End( ); sub = SW_NONE; return; } // Access curve, segment and point const uint32_t sid{ *selSegment }; const uint32_t pid{ *selPoint }; auto& sync{ Common::Sync( ) }; auto* const ovr{ selId->isOverride ? sync.getOverride( selId->id ) : nullptr }; auto const* const curve{ selUpdatingCopies.size( ) ? &selUpdatingCopies[ 0 ] : sync.getCurve( ovr ? ovr->inputNames( )[ 0 ] : selId->id ) }; auto const& segment{ curve->segments[ sid ] }; if ( selId->isOverride ) { Text( "Override:" ); SameLine( 110 ); Text( "%s" , ovr->title( ) ); } else { Text( "Curve:" ); SameLine( 110 ); Text( "%s" , selId->id.toOSString( ).data( ) ); } Text( "Segment index:" ); SameLine( 110 ); Text( "%d" , sid ); Text( "Point index:" ); SameLine( 110 ); Text( "%d / %d" , pid , segment.durations.size( ) ); Separator( ); // Find start and duration uint32_t segStart{ 0 } , pointPosRel{ 0 }; for ( auto i = 0u ; i <= sid ; i ++ ) { auto const& s{ curve->segments[ i ] }; auto& tgt{ i == sid ? pointPosRel : segStart }; const auto end{ i == sid ? pid : s.durations.size( ) }; for ( auto j = 0u ; j < end ; j ++ ) { tgt += s.durations[ j ]; } } const uint32_t pointPosAbs{ segStart + pointPosRel }; const float tPointPosAbs{ sync.durationUnitSize( ) * pointPosAbs } , tPointPosRel{ sync.durationUnitSize( ) * pointPosRel }; Text( "Absolute:" ); SameLine( 110 ); Text( "%d units" , pointPosAbs ); Text( " " ); SameLine( 110 ); Text( "%.3f seconds" , tPointPosAbs ); Text( "Relative:" ); SameLine( 110 ); Text( "%d units" , pointPosRel ); Text( " " ); SameLine( 110 ); Text( "%.3f seconds" , tPointPosRel ); Separator( ); // This is here because the curve pointer might get fucked over by // the override control const auto canInsertBefore{ pid != 0 && segment.durations[ pid - 1 ] > 1 }; const auto canInsertAfter{ pid != segment.durations.size( ) && segment.durations[ pid ] > 1 }; const auto canDelete{ pid != 0 && pid != segment.durations.size( ) }; bool changed; float value{ 0 }; if ( ovr ) { selEditor.use( ); uint32_t i{ 0 }; changed = (UI::Sync( ).uiFor( *ovr ))( *ovr , selEditor , i , stringBuffer ); if ( Button( "Copy data" , ImVec2{ -1 , 0 } ) && !changed ) { auto const& iNames{ ovr->inputNames( ) }; cpType = ovr->type( ); cpData.clear( ); for ( auto i = 0u ; i < iNames.size( ) ; i ++ ) { cpData.add( sync.getCurve( iNames[ i ] )->segments[ sid ].values[ pid ] ); } } if ( cpType != ovr->type( ) ) { PushDisabled( ); } if ( Button( "Paste data" , ImVec2{ -1 , 0 } ) && !changed ) { auto const& iNames{ ovr->inputNames( ) }; for ( auto i = 0u ; i < iNames.size( ) ; i ++ ) { auto c{ *sync.getCurve( iNames[ i ] ) }; selUpdatingOriginals.add( c ); c.segments[ sid ].values[ pid ] = cpData[ i ]; selUpdatingCopies.add( c ); sync.setCurve( std::move( c ) ); } selUpdate = E_ChangeType::POINT_VALUE; } if ( cpType != ovr->type( ) ) { PopDisabled( ); } } else { Text( "Value:" ); SameLine( 110 ); PushItemWidth( -1 ); value = segment.values[ pid ]; DragFloat( "##value" , &value , .01f , 0 , 0 , "%.6f" ); changed = IsItemActive( ); PopItemWidth( ); } const bool canUseButtons{ !changed && selUpdate == E_ChangeType::NONE }; if ( canDelete ) { SameLine( 110 ); if ( Button( "Delete point" , ImVec2{ -1 , 0 } ) && canUseButtons ) { SyncEditor::DeletePoint( *selId , sid , pid ); selPoint.clear( ); sub = SW_SEGMENT; } } if ( canInsertAfter || canInsertBefore ) { Separator( ); if ( canInsertBefore ) { if ( Button( "Insert before" , ImVec2{ -1 , 0 } ) && canUseButtons ) { SyncEditor::InsertPoint( *selId , sid , pid ); (*selPoint) ++; } } if ( canInsertAfter ) { if ( Button( "Insert after" , ImVec2{ -1 , 0 } ) && canUseButtons ) { SyncEditor::InsertPoint( *selId , sid , pid + 1 ); } } } if ( ovr && changed ) { if ( selUpdate == E_ChangeType::NONE ) { selUpdate = E_ChangeType::POINT_VALUE; } else { assert( selUpdate == E_ChangeType::POINT_VALUE ); } } else if ( changed ) { if ( selUpdate == E_ChangeType::NONE ) { assert( selUpdatingOriginals.empty( ) ); assert( selUpdatingCopies.empty( ) ); selUpdatingOriginals.add( *curve ); selUpdatingCopies.add( *curve ); selUpdate = E_ChangeType::POINT_VALUE; } else { assert( selUpdate == E_ChangeType::POINT_VALUE ); } selUpdatingCopies[ 0 ].segments[ sid ].values[ pid ] = value; sync.setCurve( selUpdatingCopies[ 0 ] ); } else if ( selUpdate == E_ChangeType::POINT_VALUE ) { selUpdate = E_ChangeType::NONE; auto& undo{ dynamic_cast< T_UndoSyncChanges& >( Common::Undo( ).add< T_UndoSyncChanges >( ) ) }; for ( auto i = 0u ; i < selUpdatingCopies.size( ) ; i ++ ) { undo.curveReplacement( std::move( selUpdatingOriginals[ i ] ) , std::move( selUpdatingCopies[ i ] ) ); } selUpdatingCopies.clear( ); selUpdatingOriginals.clear( ); } End( ); } } // namespace /*= T_SyncView ===============================================================*/ T_SyncView::T_SyncView( ) noexcept : A_PrivateImplementation( new T_SyncViewImpl_( ) ) { } bool T_SyncView::display( ) noexcept { return p< T_SyncViewImpl_ >( ).display( ); }