diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /utils/hlfaceposer/timelineitem.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'utils/hlfaceposer/timelineitem.cpp')
| -rw-r--r-- | utils/hlfaceposer/timelineitem.cpp | 1763 |
1 files changed, 1763 insertions, 0 deletions
diff --git a/utils/hlfaceposer/timelineitem.cpp b/utils/hlfaceposer/timelineitem.cpp new file mode 100644 index 0000000..50a4b86 --- /dev/null +++ b/utils/hlfaceposer/timelineitem.cpp @@ -0,0 +1,1763 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#include "hlfaceposer.h" +#include <stdio.h> +#include "TimelineItem.h" +#include "choreowidgetdrawhelper.h" +#include "mathlib/mathlib.h" +#include "expressions.h" +#include "StudioModel.h" +#include "expclass.h" +#include "mathlib/mathlib.h" +#include "ExpressionTool.h" +#include "choreoevent.h" +#include "choreoscene.h" +#include "choreoactor.h" +#include "choreochannel.h" +#include "ChoreoView.h" +#include "ControlPanel.h" +#include "faceposer_models.h" +#include "MatSysWin.h" +#include "choreoviewcolors.h" +#include "ifaceposersound.h" +#include "curveeditorhelpers.h" + +extern double realtime; + +#define DOUBLE_CLICK_TIME 0.2 + +#define GROW_HANDLE_WIDTH 66 +#define GROW_HANDLE_HEIGHT 8 +#define GROW_HANDLE_INSETPIXELS 8 + +#define TIMELINEITEM_DEFAULT_HEIGHT 100 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +TimelineItem::TimelineItem( mxWindow *workspace ) +{ + m_pHelper = new CCurveEditorHelper< TimelineItem >( this ); + + m_pWorkspace = workspace; + + m_nDragging = DRAGTYPE_NONE; + m_nLastX = 0; + m_nLastY = 0; + m_nStartX = 0; + m_nStartY = 0; + + SetExpressionInfo( NULL, 0 ); + + m_nNumSelected = 0; + + m_nEditType = 0; + + SetCollapsed( false ); + SetActive( false ); + + m_nUndoSetup = 0; + m_rcBounds.top = 0; + m_rcBounds.bottom = 0; + m_rcBounds.left = 0; + m_rcBounds.right = 0; + m_bVisible = false; + m_flLastClickTime = -1; + + m_nCurrentHeight = TIMELINEITEM_DEFAULT_HEIGHT; +} + +TimelineItem::~TimelineItem( void ) +{ + delete m_pHelper; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::ResetHeight() +{ + m_nCurrentHeight = TIMELINEITEM_DEFAULT_HEIGHT; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : mx - +// Output : float +//----------------------------------------------------------------------------- +float TimelineItem::GetTimeForMouse( int mx, bool clip /*= false*/ ) +{ + float start, end; + g_pExpressionTool->GetStartAndEndTime( start, end ); + + if ( clip ) + { + if ( mx < m_rcBounds.left ) + { + return start; + } + else if ( mx >= m_rcBounds.right ) + { + return end; + } + } + + float frac = (float)( mx - m_rcBounds.left ) / (float)( m_rcBounds.right - m_rcBounds.left ); + float t = start + frac * ( end - start ); + return t; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : t - +// Output : int +//----------------------------------------------------------------------------- +int TimelineItem::GetMouseForTime( float t, bool *clipped /*= NULL*/ ) +{ + float start, end; + g_pExpressionTool->GetStartAndEndTime( start, end ); + + float frac = ( t - start ) / ( end - start ); + + if ( frac < 0.0 || frac > 1.0 ) + { + if ( clipped ) + { + *clipped = true; + } + } + + int mx = m_rcBounds.left + ( int ) ( frac * (float)( m_rcBounds.right - m_rcBounds.left ) ); + + return mx; +} + +int TimelineItem::NumSamples() +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return 0; + + // Aggregate both types of tracks together + return track->GetNumSamples( 0 ) + track->GetNumSamples( 1 ); + // return track->GetNumSamples( m_nEditType ); +} + +CExpressionSample *TimelineItem::GetSample( int idx ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return NULL; + + if ( idx >= track->GetNumSamples( 0 ) ) + { + // Rebase and look at left/right track instead + idx -= track->GetNumSamples( 0 ); + return track->GetSample( idx, 1 ); + } + return track->GetSample( idx, 0 ); +} + +CExpressionSample *TimelineItem::GetSampleUnderMouse( int mx, int my, float tolerance /*= FP_TL_SELECTION_TOLERANCE*/ ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return NULL; + + CChoreoEvent *e = track->GetEvent(); + if ( !e ) + return NULL; + + float closest_dist = 9999999.f; + CExpressionSample *bestsample = NULL; + + // Add a sample point + int height = m_rcBounds.bottom - m_rcBounds.top; + + mx += m_rcBounds.left; + + for ( int i = 0; i < track->GetNumSamples( m_nEditType ); i++ ) + { + CExpressionSample *sample = track->GetSample( i, m_nEditType ); + + bool clipped = false; + int px = GetMouseForTime( sample->time, &clipped ); + int py = height * ( 1.0f - sample->value ); + + int dx = px - mx; + int dy = py - my; + + float dist = sqrt( (float)(dx * dx + dy * dy) ); + if ( dist < closest_dist ) + { + bestsample = sample; + closest_dist = dist; + } + } + + // Not close to any of them!!! + if ( ( tolerance != 0.0f ) && + ( closest_dist > tolerance ) ) + return NULL; + + return bestsample; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::DeselectAll( void ) +{ + g_pExpressionTool->DeselectAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::SelectAll( void ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + for ( int t = 0; t < 2; t++ ) + { + for ( int i = 0; i < track->GetNumSamples( t ); i++ ) + { + CExpressionSample *sample = track->GetSample( i, t ); + sample->selected = true; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::Delete( void ) +{ + g_pExpressionTool->DeleteSelectedSamples(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : sample - +//----------------------------------------------------------------------------- +void TimelineItem::AddSample( CExpressionSample const& sample ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + PreDataChanged( "Add sample point" ); + + track->AddSample( sample.time, sample.value, m_nEditType ); + track->Resort( m_nEditType ); + + SetActive( true ); + + PostDataChanged( "Add sample point" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int TimelineItem::CountSelected( void ) +{ + m_nNumSelected = m_pHelper->CountSelected( false ); + return m_nNumSelected; +} + +void TimelineItem::SetMousePositionForEvent( mxEvent *event ) +{ + POINT pt; + GetCursorPos( &pt ); + ScreenToClient( (HWND)m_pWorkspace->getHandle(), &pt ); + + pt.x -= m_rcBounds.left; + pt.y -= m_rcBounds.top; + + event->x = pt.x; + event->y = pt.y; +} + +int TimelineItem::handleEvent( mxEvent *event ) +{ + int iret = 0; + + // Give helper a shot at the event + if ( m_pHelper->HelperHandleEvent( event ) ) + { + return 1; + } + + switch ( event->event ) + { + case mxEvent::KeyDown: + { + switch ( event->key ) + { + default: + iret = g_pChoreoView->HandleZoomKey( g_pExpressionTool, event->key ); + break; + case VK_ESCAPE: + DeselectAll(); + DrawSelf(); + break; + case VK_DELETE: + Delete(); + DrawSelf(); + break; + case 'C': + Copy(); + DrawSelf(); + break; + case 'V': + Paste(); + DrawSelf(); + break; + case 'J': + { + g_pExpressionTool->OnCopyToFlex( g_pExpressionTool->GetScrubberSceneTime(), true ); + } + break; + case 'K': + { + g_pExpressionTool->OnCopyFromFlex( g_pExpressionTool->GetScrubberSceneTime(), false ); + } + break; + case 188: // VK_OEM_COMMA: + { + g_pExpressionTool->SetScrubTargetTime( 0.0f ); + } + break; + case 190: // VK_OEM_PERIOD: + { + CChoreoScene *scene = g_pChoreoView->GetScene(); + if ( scene ) + { + g_pExpressionTool->SetScrubTargetTime( scene->FindStopTime() ); + } + } + break; + case VK_LEFT: + { + CChoreoScene *scene = g_pChoreoView->GetScene(); + if ( scene && scene->GetSceneFPS() > 0 ) + { + float curscrub = g_pExpressionTool->GetScrub(); + curscrub -= ( 1.0f / (float)scene->GetSceneFPS() ); + curscrub = max( curscrub, 0.0f ); + g_pExpressionTool->SetScrubTargetTime( curscrub ); + } + } + break; + case VK_RIGHT: + { + CChoreoScene *scene = g_pChoreoView->GetScene(); + if ( scene && scene->GetSceneFPS() > 0 ) + { + float curscrub = g_pExpressionTool->GetScrub(); + curscrub += ( 1.0f / (float)scene->GetSceneFPS() ); + curscrub = min( curscrub, scene->FindStopTime() ); + g_pExpressionTool->SetScrubTargetTime( curscrub ); + } + } + break; + case 191: + { + if ( g_pChoreoView->IsPlayingScene() ) + { + g_pChoreoView->StopScene(); + } + } + break; + } + iret = 1; + } + break; + case mxEvent::KeyUp: + { + switch ( event->key ) + { + case VK_SPACE: + { + CFlexAnimationTrack *track = GetSafeTrack(); + if ( track && track->IsComboType() ) + { + SetEditType( m_nEditType == 0 ? 1 : 0 ); + DrawSelf(); + } + } + break; + } + iret = 1; + } + break; + case mxEvent::MouseDown: + { + sound->Flush(); + + SetFocus( (HWND)g_pExpressionTool->getHandle() ); + + int height = m_rcBounds.bottom - m_rcBounds.top; + + bool rightbutton = ( event->buttons & mxEvent::MouseRightButton ) ? true : false; + + if ( m_nDragging == DRAGTYPE_NONE ) + { + bool ctrlDown = ( event->modifiers & mxEvent::KeyCtrl ) ? true : false; + + CExpressionSample *sample = GetSampleUnderMouse( event->x, event->y, ctrlDown ? FP_TL_ADDSAMPLE_TOLERANCE : FP_TL_SELECTION_TOLERANCE ); + + if ( IsMouseOverGrowHandle( (short)event->x, (short)event->y ) ) + { + m_nDragging = DRAGTYPE_GROW; + m_nLastX = (short)event->x; + m_nLastY = (short)event->y; + + m_nStartX = m_nLastX; + m_nStartY = m_nLastY; + + MouseDrag( (short)event->x, (short)event->y, event->modifiers ); + + DrawGrowRect(); + } + else if ( sample ) + { + if ( event->modifiers & mxEvent::KeyShift ) + { + sample->selected = !sample->selected; + DrawSelf(); + } + else if ( sample->selected ) + { + m_nDragging = rightbutton ? DRAGTYPE_MOVEPOINTS_TIME : DRAGTYPE_MOVEPOINTS_VALUE; + m_nLastX = (short)event->x; + m_nLastY = (short)event->y; + + m_nStartX = m_nLastX; + m_nStartY = m_nLastY; + + PreDataChanged( "Move sample point(s)" ); + + MouseDrag( (short)event->x, (short)event->y, event->modifiers ); + + DrawSelf(); + } + else + { + if ( !( event->modifiers & mxEvent::KeyShift ) ) + { + DeselectAll(); + DrawSelf(); + } + + m_nDragging = DRAGTYPE_SELECTION; + m_nLastX = (short)event->x; + m_nLastY = (short)event->y; + + m_nStartX = m_nLastX; + m_nStartY = m_nLastY; + + MouseDrag( (short)event->x, (short)event->y, event->modifiers ); + + DrawFocusRect(); + } + } + else if ( event->modifiers & mxEvent::KeyCtrl ) + { + CChoreoEvent *e = g_pExpressionTool->GetSafeEvent(); + if ( e ) + { + // Add a sample point + float t = GetTimeForMouse( (short)event->x + m_rcBounds.left ); + + CExpressionSample sample; + sample.time = FacePoser_SnapTime( t ); + sample.value = 1.0f - (float)( (short)( event->y ) ) / (float)height; + sample.selected = false; + + AddSample( sample ); + + DrawSelf(); + } + } + else + { + if ( rightbutton ) + { + POINT pt; + pt.x = event->x; + pt.y = event->y; + ClientToScreen( (HWND)m_pWorkspace->getHandle(), &pt ); + ScreenToClient( (HWND)g_pExpressionTool->getHandle(), &pt ); + event->x = pt.x; + event->y = pt.y; + g_pExpressionTool->ShowContextMenu( event, true ); + return iret; + } + + if ( !( event->modifiers & mxEvent::KeyShift ) ) + { + DeselectAll(); + DrawSelf(); + } + + m_nDragging = DRAGTYPE_SELECTION; + m_nLastX = (short)event->x; + m_nLastY = (short)event->y; + + m_nStartX = m_nLastX; + m_nStartY = m_nLastY; + + MouseDrag( (short)event->x, (short)event->y, event->modifiers ); + + DrawFocusRect(); + } + } + iret = 1; + } + break; + case mxEvent::MouseDrag: + case mxEvent::MouseMove: + { + if ( m_nDragging != DRAGTYPE_NONE ) + { + if ( m_nDragging == DRAGTYPE_SELECTION ) + { + DrawFocusRect(); + } + else if ( m_nDragging == DRAGTYPE_GROW ) + { + DrawGrowRect(); + } + + MouseDrag( (short)event->x, (short)event->y, event->modifiers ); + + if ( m_nDragging == DRAGTYPE_SELECTION ) + { + DrawFocusRect(); + } + else if ( m_nDragging == DRAGTYPE_GROW ) + { + DrawGrowRect(); + } + + if ( m_nDragging == DRAGTYPE_MOVEPOINTS_TIME || + m_nDragging == DRAGTYPE_MOVEPOINTS_VALUE ) + { + DrawSelf(); + } + } + else + { + // See if anything is selected + CountSelected(); + if ( m_nNumSelected <= 0 && g_pExpressionTool->IsFocusItem( this ) ) + { + // Nothing selected + // Draw auto highlight + DrawAutoHighlight( event ); + } + } + iret = 1; + } + break; + case mxEvent::MouseUp: + { + bool overgrow = IsMouseOverGrowHandle( (short)event->x, (short)event->y ); + + if ( m_nDragging != DRAGTYPE_NONE ) + { + if ( m_nDragging == DRAGTYPE_SELECTION ) + { + DrawFocusRect(); + } + else if ( m_nDragging == DRAGTYPE_GROW ) + { + DrawGrowRect(); + } + + MouseDrag( (short)event->x, (short)event->y, event->modifiers, true ); + + if ( m_nDragging == DRAGTYPE_GROW ) + { + // Finish grow by resizing control + int desiredheight = m_nCurrentHeight + event->y - m_nStartY; + if ( desiredheight >= 10 ) + { + m_nCurrentHeight = desiredheight; + g_pExpressionTool->LayoutItems( true ); + } + } + else if ( m_nDragging != DRAGTYPE_MOVEPOINTS_VALUE && + m_nDragging != DRAGTYPE_MOVEPOINTS_TIME ) + { + SelectPoints(); + } + else + { + PostDataChanged( "Move sample point(s)" ); + } + + m_nDragging = DRAGTYPE_NONE; + + DrawSelf(); + } + + bool rightbutton = ( event->buttons & mxEvent::MouseRightButton ) ? true : false; + bool shift = ( event->modifiers & mxEvent::KeyShift ) ? true : false; + bool ctrl = ( event->modifiers & mxEvent::KeyCtrl ) ? true : false; + + if ( !rightbutton && !shift && !ctrl ) + { + if ( realtime - m_flLastClickTime < DOUBLE_CLICK_TIME ) + { + if ( overgrow || IsCollapsed() ) + { + OnDoubleClicked(); + } + } + + m_flLastClickTime = realtime; + } + + iret = 1; + } + break; + } + + return iret; +} + +void TimelineItem::MouseDrag( int x, int y, int modifiers, bool snap /*=false*/ ) +{ + if ( m_nDragging == DRAGTYPE_NONE ) + return; + + int width = m_rcBounds.right - m_rcBounds.left; + int height = m_rcBounds.bottom - m_rcBounds.top; + + if ( m_nDragging == DRAGTYPE_MOVEPOINTS_TIME || + m_nDragging == DRAGTYPE_MOVEPOINTS_VALUE ) + { + int dx = x - m_nLastX; + int dy = y - m_nLastY; + + if ( !( modifiers & mxEvent::KeyCtrl ) ) + { + // Zero out motion on other axis + if ( m_nDragging == DRAGTYPE_MOVEPOINTS_VALUE ) + { + dx = 0; + x = m_nLastX; + } + else + { + dy = 0; + y = m_nLastY; + } + } + + float dfdx = (float)dx / g_pExpressionTool->GetPixelsPerSecond(); + float dfdy = (float)dy / (float)height; + + g_pExpressionTool->MoveSelectedSamples( dfdx, dfdy, snap ); + + // Update the scrubber + if ( (float)width > 0 ) + { + float t = GetTimeForMouse( x + m_rcBounds.left ); + g_pExpressionTool->ForceScrubPosition( t ); + + g_pMatSysWindow->Frame(); + } + } + + m_nLastX = x; + m_nLastY = y; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::DrawFocusRect( void ) +{ + RECT rcFocus; + + rcFocus.left = m_nStartX < m_nLastX ? m_nStartX : m_nLastX; + rcFocus.right = m_nStartX < m_nLastX ? m_nLastX : m_nStartX; + + rcFocus.top = m_nStartY < m_nLastY ? m_nStartY : m_nLastY; + rcFocus.bottom = m_nStartY < m_nLastY ? m_nLastY : m_nStartY; + + POINT offset; + offset.x = m_rcBounds.left; + offset.y = m_rcBounds.top; + ClientToScreen( (HWND)m_pWorkspace->getHandle(), &offset ); + OffsetRect( &rcFocus, offset.x, offset.y ); + + HDC dc = GetDC( NULL ); + + ::DrawFocusRect( dc, &rcFocus ); + + ReleaseDC( NULL, dc ); +} + +void TimelineItem::DrawSelf( void ) +{ + CChoreoWidgetDrawHelper drawHelper( m_pWorkspace, m_rcBounds ); + Draw( drawHelper ); +} + +void TimelineItem::Draw( CChoreoWidgetDrawHelper& drawHelper ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + CChoreoEvent *e = track->GetEvent(); + if ( !e ) + return; + + Assert( e->HasEndTime() ); + + bool active = track && ( IsValid() || IsActive() ); + + float starttime; + float endtime; + + g_pExpressionTool->GetStartAndEndTime( starttime, endtime ); + + CountSelected(); + int scount = GetNumSelected(); + + COLORREF bgColor = RGB( 230, 230, 200 ); + if ( IsCollapsed() && active ) + { + bgColor = RGB( 200, 230, 200 ); + } + + RECT rcClient = m_rcBounds; + + drawHelper.DrawFilledRect( bgColor, rcClient ); + + COLORREF gray = RGB( 200, 200, 200 ); + + DrawEventEnd( drawHelper ); + + DrawRelativeTags( drawHelper ); + if ( !IsCollapsed() && track ) + { + if ( m_nEditType == 1 ) + { + float zero = track->GetZeroValue( m_nEditType, true ); + + drawHelper.DrawColoredLine( RGB( 180, 200, 220 ), PS_SOLID, 1, + rcClient.left, ( rcClient.top * zero + rcClient.bottom * (1 - zero)) , + rcClient.right, ( rcClient.top * zero + rcClient.bottom * (1 - zero)) ); + } + + drawHelper.DrawOutlinedRect( RGB( 100, 150, 200 ), PS_SOLID, 1, rcClient ); + + // Draw grow handle into background... + if ( CanHaveGrowHandle() ) + { + RECT handleRect; + GetGrowHandleRect( handleRect ); + DrawGrowHandle( drawHelper, handleRect ); + } + + // Draw left/right underneath amount so go backbard + for ( int type = ( track->IsComboType() ? 1 : 0 ); type >= 0; type-- ) + { + COLORREF lineColor = ( type == m_nEditType ) ? RGB( 0, 0, 255 ) : gray; + COLORREF shadowColor = ( type == m_nEditType ) ? RGB( 150, 150, 250 ) : gray; + COLORREF dotColor = ( type == m_nEditType ) ? RGB( 0, 0, 255 ) : gray; + COLORREF dotColorSelected = ( type == m_nEditType ) ? RGB( 240, 80, 20 ) : gray; + + int height = rcClient.bottom - rcClient.top; + int bottom = rcClient.bottom; + + // Fixme, could look at 1st derivative and do more sampling at high rate of change? + // or near actual sample points! + float linelength = g_pExpressionTool->IsFocusItem( this ) ? 2.0f : 8.0f; + + float timestepperpixel = linelength / g_pExpressionTool->GetPixelsPerSecond(); + + float stoptime = min( endtime, e->GetDuration() ); + + float prev_t = starttime; + float prev_value = track->GetFracIntensity( prev_t, type ); + + CUtlVector< POINT > segments; + + /* + if (type == m_nEditType) + { + // draw hermite version of time step + float i0, i1, i2; + float time10hz = starttime; + + i0 = track->GetFracIntensity( time10hz, type ); + i1 = i0; + time10hz = starttime + 0.1; + i2 = track->GetFracIntensity( time10hz, type );; + + for ( float t = starttime; t <= stoptime; t += timestepperpixel ) + { + while (t >= time10hz) + { + time10hz += 0.1; + i0 = i1; + i1 = i2; + i2 = track->GetFracIntensity( time10hz, type );; + } + + float value = Hermite_Spline( i0, i1, i2, (t - time10hz + 0.1) / 0.1 ); + + int prevx, x; + + bool clipped1, clipped2; + x = GetMouseForTime( t, &clipped1 ); + prevx = GetMouseForTime( prev_t, &clipped2 ); + + //if ( !clipped1 && !clipped2 ) + { + // Draw segment + //drawHelper.DrawColoredLine( lineColor, PS_SOLID, 1, + // prevx, bottom - prev_value * height, + // x, bottom - value * height ); + + POINT pt; + + if ( segments.Count() == 0 ) + { + pt.x = prevx; + pt.y = bottom - prev_value * height; + + segments.AddToTail( pt ); + } + + pt.x = x; + pt.y = bottom - value * height; + + segments.AddToTail( pt ); + } + + prev_t = t; + prev_value = value; + } + + if ( segments.Count() >= 2 ) + { + drawHelper.DrawColoredPolyLine( shadowColor, PS_SOLID, 1, segments ); + } + + segments.RemoveAll(); + } + */ + for ( float t = starttime; t <= stoptime; t += timestepperpixel ) + { + float value = track->GetFracIntensity( t, type ); + + int prevx, x; + + bool clipped1, clipped2; + x = GetMouseForTime( t, &clipped1 ); + prevx = GetMouseForTime( prev_t, &clipped2 ); + + //if ( !clipped1 && !clipped2 ) + { + // Draw segment + //drawHelper.DrawColoredLine( lineColor, PS_SOLID, 1, + // prevx, bottom - prev_value * height, + // x, bottom - value * height ); + + POINT pt; + + if ( segments.Count() == 0 ) + { + pt.x = prevx; + pt.y = bottom - prev_value * height; + + segments.AddToTail( pt ); + } + + pt.x = x; + pt.y = bottom - value * height; + + segments.AddToTail( pt ); + } + + prev_t = t; + prev_value = value; + } + + if ( segments.Count() >= 2 ) + { + drawHelper.DrawColoredPolyLine( lineColor, PS_SOLID, 1, segments ); + } + + for ( int sample = 0; sample < track->GetNumSamples( type ); sample++ ) + { + bool dummy; + CExpressionSample *start = track->GetBoundedSample( sample, dummy, type ); + + /* + int pixel = (int)( ( start->time / event_time ) * width + 0.5f); + int x = m_rcBounds.left + pixel; + float roundedfrac = (float)pixel / (float)width; + */ + float value = start->value; // track->GetFracIntensity( start->time, type ); + bool clipped = false; + int x = GetMouseForTime( start->time, &clipped ); + if ( clipped ) + continue; + int y = bottom - value * height; + + int dotsize = 6; + int dotSizeSelected = 6; + + COLORREF clr = dotColor; + COLORREF clrSelected = dotColorSelected; + + drawHelper.DrawCircle( + start->selected ? clrSelected : clr, + x, y, + start->selected ? dotSizeSelected : dotsize, + true ); + + + if ( !start->selected ) + continue; + + if ( start->GetCurveType() == CURVE_DEFAULT ) + continue; + + // Draw curve type indicator... + char sz[ 128 ]; + Q_snprintf( sz, sizeof( sz ), "%s", Interpolator_NameForCurveType( start->GetCurveType(), true ) ); + RECT rc; + int fontSize = 9; + rc.top = clamp( y + 5, rcClient.top + 2, rcClient.bottom - 2 - fontSize ); + rc.bottom = rc.top + fontSize + 1; + rc.left = x - 75; + rc.right = x + 175; + drawHelper.DrawColoredText( "Arial", fontSize, 500, shadowColor, rc, sz ); + } + } + } + + if ( track && track->IsComboType() && !IsCollapsed() ) + { + RECT title = rcClient; + title.left += 10; + title.top += 14; + title.bottom = title.top + 9; + + char sz[ 128 ]; + + if ( m_nEditType == 1 ) + { + sprintf( sz, "left" ); + + drawHelper.DrawColoredText( "Arial", 9, 500, RGB( 0, 0, 255 ), title, sz ); + + sprintf( sz, "right" ); + + title.top = rcClient.bottom - 22; + title.bottom = rcClient.bottom; + + drawHelper.DrawColoredText( "Arial", 9, 500, RGB( 0, 0, 255 ), title, sz ); + } + + int mid = ( rcClient.top + rcClient.bottom ) / 2; + + title.top = mid - 10; + title.bottom = mid; + + sprintf( sz, "editmode: <%s>", m_nEditType == 0 ? "amount" : "left/right" ); + + drawHelper.DrawColoredText( "Arial", 9, 500, RGB( 0, 0, 255 ), title, sz ); + } + + if ( track ) + { + RECT title = rcClient; + title.left += 2; + title.top += 2; + title.bottom = title.top + 9; + + char const *name = track->GetFlexControllerName(); + char sz[ 128 ]; + + if ( scount > 0 ) + { + sprintf( sz, "{%i} - ", scount ); + + int len = drawHelper.CalcTextWidth( "Arial", 9, 500, sz ); + drawHelper.DrawColoredText( "Arial", 9, 500, + RGB( 120, 120, 0 ), title, sz ); + + title.left += len + 2; + } + + sprintf( sz, "%s -", name ); + + int len = drawHelper.CalcTextWidth( "Arial", 9, 500, sz ); + + drawHelper.DrawColoredText( "Arial", 9, 500, + active ? RGB( 0, 150, 100 ) : RGB( 100, 100, 100 ), + title, sz ); + + sprintf( sz, "%s", IsActive() ? "enabled" : "disabled" ); + + title.left += len + 2; + + len = drawHelper.CalcTextWidth( "Arial", 9, 500, sz ); + drawHelper.DrawColoredText( "Arial", 9, 500, + active ? RGB( 0, 150, 100 ) : RGB( 100, 100, 100 ), + title, sz ); + + if ( active ) + { + title.left += len + 2; + + sprintf( sz, " <%i>", track->GetNumSamples( 0 ) ); + + len = drawHelper.CalcTextWidth( "Arial", 9, 500, sz ); + drawHelper.DrawColoredText( "Arial", 9, 500, RGB( 220, 0, 00 ), title, sz ); + } + } +} + +void TimelineItem::DrawAutoHighlight( mxEvent *event ) +{ + if ( IsCollapsed() ) + return; + + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + CExpressionSample *hover = GetSampleUnderMouse( event->x, event->y, 0.0f ); + CChoreoWidgetDrawHelper drawHelper( m_pWorkspace, m_rcBounds, true ); + + RECT rcClient = m_rcBounds; + + // Draw left/right underneath amount so go backbard + int type = m_nEditType; + + COLORREF dotColor = RGB( 0, 0, 255 ); + COLORREF dotColorSelected = RGB( 240, 80, 20 ); + COLORREF clrHighlighted = RGB( 0, 200, 0 ); + + int height = rcClient.bottom - rcClient.top; + int bottom = rcClient.bottom; + + int dotsize = 6; + int dotSizeSelected = 6; + int dotSizeHighlighted = 6; + + COLORREF clr = dotColor; + COLORREF clrSelected = dotColorSelected; + COLORREF bgColor = RGB( 230, 230, 200 ); + + // Fixme, could look at 1st derivative and do more sampling at high rate of change? + // or near actual sample points! + for ( int sample = 0; sample < track->GetNumSamples( type ); sample++ ) + { + bool dummy; + CExpressionSample *start = track->GetBoundedSample( sample, dummy, type ); + + float value = start->value; + bool clipped = false; + int x = GetMouseForTime( start->time, &clipped ); + if ( clipped ) + continue; + int y = bottom - value * height; + + if ( hover == start ) + { + drawHelper.DrawCircle( + bgColor, + x, y, + dotSizeHighlighted, + true ); + + drawHelper.DrawCircle( + clrHighlighted, + x, y, + dotSizeHighlighted, + false ); + + + } + else + { + drawHelper.DrawCircle( + start->selected ? clrSelected : clr, + x, y, + start->selected ? dotSizeSelected : dotsize, + true ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : drawHelper - +//----------------------------------------------------------------------------- +void TimelineItem::DrawRelativeTags( CChoreoWidgetDrawHelper& drawHelper ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + CChoreoEvent *event = track->GetEvent(); + if ( !event ) + return; + + float duration = event->GetDuration(); + + if ( duration <= 0.0f ) + return; + + CChoreoScene *scene = g_pChoreoView->GetScene(); + if ( !scene ) + return; + + RECT rcClient = m_rcBounds;; + //drawHelper.GetClientRect( rcClient ); + + // Iterate relative tags + for ( int i = 0; i < scene->GetNumActors(); i++ ) + { + CChoreoActor *a = scene->GetActor( i ); + if ( !a ) + continue; + + for ( int j = 0; j < a->GetNumChannels(); j++ ) + { + CChoreoChannel *c = a->GetChannel( j ); + if ( !c ) + continue; + + for ( int k = 0 ; k < c->GetNumEvents(); k++ ) + { + CChoreoEvent *e = c->GetEvent( k ); + if ( !e ) + continue; + + // add each tag to combo box + for ( int t = 0; t < e->GetNumRelativeTags(); t++ ) + { + CEventRelativeTag *tag = e->GetRelativeTag( t ); + if ( !tag ) + continue; + + //SendMessage( control, CB_ADDSTRING, 0, (LPARAM)va( "\"%s\" \"%s\"", tag->GetName(), e->GetParameters() ) ); + bool clipped = false; + int tagx = GetMouseForTime( tag->GetStartTime() - event->GetStartTime(), &clipped ); + if ( clipped ) + continue; + + drawHelper.DrawColoredLine( RGB( 180, 180, 220 ), PS_SOLID, 1, tagx, rcClient.top, tagx, rcClient.bottom ); + } + } + } + } + + for ( int t = 0; t < event->GetNumTimingTags(); t++ ) + { + CFlexTimingTag *tag = event->GetTimingTag( t ); + if ( !tag ) + continue; + + bool clipped = false; + int tagx = GetMouseForTime( tag->GetStartTime() - event->GetStartTime(), &clipped ); + if ( clipped ) + continue; + + // Draw relative tag marker + drawHelper.DrawColoredLine( RGB( 220, 180, 180 ), PS_SOLID, 1, tagx, rcClient.top, tagx, rcClient.bottom ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *exp - +// flexnum - +//----------------------------------------------------------------------------- +void TimelineItem::SetExpressionInfo( CFlexAnimationTrack *track, int flexnum ) +{ + m_szTrackName[ 0 ] = 0; + if ( track ) + { + V_strcpy_safe( m_szTrackName, track->GetFlexControllerName() ); + SetActive( track->IsTrackActive() ); + } + + m_nFlexNum = flexnum; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::Copy( void ) +{ + if ( !g_pExpressionTool ) + return; + + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + g_pExpressionTool->Copy( track ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::Paste( void ) +{ + if ( !g_pExpressionTool ) + return; + + if ( !g_pExpressionTool->HasCopyData() ) + return; + + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + g_pExpressionTool->Paste( track ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::Clear( bool preserveundo ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + if ( preserveundo ) + { + PreDataChanged( "Clear" ); + } + + track->Clear(); + + if ( preserveundo ) + { + PostDataChanged( "Clear" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +void TimelineItem::SetCollapsed( bool state ) +{ + m_bCollapsed = state; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool TimelineItem::IsCollapsed( void ) const +{ + return m_bCollapsed; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : int +//----------------------------------------------------------------------------- +int TimelineItem::GetHeight( void ) +{ + return ( IsCollapsed() ? 12 : m_nCurrentHeight ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +void TimelineItem::SetActive( bool state ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + track->SetTrackActive( state ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool TimelineItem::IsActive( void ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return false; + + return track->IsTrackActive(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool TimelineItem::IsValid( void ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return false; + + if ( track->GetNumSamples( 0 ) > 0 ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : CFlexAnimationTrack +//----------------------------------------------------------------------------- +CFlexAnimationTrack *TimelineItem::GetSafeTrack( void ) +{ + if ( !g_pExpressionTool ) + return NULL; + + CChoreoEvent *ev = g_pExpressionTool->GetSafeEvent(); + if ( !ev ) + return NULL; + + // Find track by name + for ( int i = 0; i < ev->GetNumFlexAnimationTracks() ; i++ ) + { + CFlexAnimationTrack *track = ev->GetFlexAnimationTrack( i ); + if ( track && !stricmp( track->GetFlexControllerName(), m_szTrackName ) ) + { + return track; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : type - +//----------------------------------------------------------------------------- +void TimelineItem::SetEditType( int type ) +{ + Assert( type == 0 || type == 1 ); + + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track || !track->IsComboType() ) + { + type = 0; + } + + m_nEditType = type; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : int +//----------------------------------------------------------------------------- +int TimelineItem::GetEditType( void ) +{ + return m_nEditType; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::SelectPoints( void ) +{ + RECT rcSelection; + + rcSelection.left = m_nStartX < m_nLastX ? m_nStartX : m_nLastX; + rcSelection.right = m_nStartX < m_nLastX ? m_nLastX : m_nStartX; + + rcSelection.top = m_nStartY < m_nLastY ? m_nStartY : m_nLastY; + rcSelection.bottom = m_nStartY < m_nLastY ? m_nLastY : m_nStartY; + + int selW = rcSelection.right - rcSelection.left; + int selH = rcSelection.bottom - rcSelection.top; + + float tolerance = FP_TL_SELECTION_RECTANGLE_TOLERANCE; + // If they are just clicking and releasing in one spot, capture any items w/in a larger tolerance + if ( selW <= 2 && selH <= 2 ) + { + tolerance = FP_TL_SELECTION_TOLERANCE; + + CExpressionSample *sample = GetSampleUnderMouse( rcSelection.left + selW * 0.5f, rcSelection.top + selH * 0.5f ); + if ( sample ) + { + sample->selected = true; + return; + } + } + else + { + InflateRect( &rcSelection, 3, 3 ); + } + + int width = m_rcBounds.right - m_rcBounds.left; + int height = m_rcBounds.bottom - m_rcBounds.top; + + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track || !width || !height ) + return; + + CChoreoEvent *e = track->GetEvent(); + Assert( e ); + if ( !e ) + return; + + float duration = e->GetDuration(); + + float fleft = (float)GetTimeForMouse( rcSelection.left + m_rcBounds.left ); + float fright = (float)GetTimeForMouse( rcSelection.right + m_rcBounds.left ); + + //fleft *= duration; + //fright *= duration; + + float ftop = (float)rcSelection.top / (float)height; + float fbottom = (float)rcSelection.bottom / (float)height; + + fleft = clamp( fleft, 0.0f, duration ); + fright = clamp( fright, 0.0f, duration ); + ftop = clamp( ftop, 0.0f, 1.0f ); + fbottom = clamp( fbottom, 0.0f, 1.0f ); + + float timestepperpixel = 1.0f / g_pExpressionTool->GetPixelsPerSecond(); + float yfracstepperpixel = 1.0f / (float)height; + + float epsx = tolerance*timestepperpixel; + float epsy = tolerance*yfracstepperpixel; + + for ( int i = 0; i < track->GetNumSamples( m_nEditType ); i++ ) + { + CExpressionSample *sample = track->GetSample( i, m_nEditType ); + + if ( sample->time + epsx < fleft ) + continue; + + if ( sample->time - epsx > fright ) + continue; + + if ( (1.0f - sample->value ) + epsy < ftop ) + continue; + + if ( (1.0f - sample->value ) - epsy > fbottom ) + continue; + + sample->selected = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *undodescription - +//----------------------------------------------------------------------------- +void TimelineItem::PreDataChanged( char const *undodescription ) +{ + if ( m_nUndoSetup == 0 ) + { + g_pChoreoView->SetDirty( true ); + g_pChoreoView->PushUndo( undodescription ); + } + ++m_nUndoSetup; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *redodescription - +//----------------------------------------------------------------------------- +void TimelineItem::PostDataChanged( char const *redodescription ) +{ + --m_nUndoSetup; + if ( m_nUndoSetup == 0 ) + { + g_pChoreoView->PushRedo( redodescription ); + g_pExpressionTool->InvalidateLayout(); + } +} + +void TimelineItem::SetBounds( const RECT& rect ) +{ + m_rcBounds = rect; +} + +void TimelineItem::GetBounds( RECT& rect ) +{ + rect = m_rcBounds; +} + +void TimelineItem::SetVisible( bool vis ) +{ + m_bVisible = vis; +} + +bool TimelineItem::GetVisible( void ) const +{ + return m_bVisible; +} + +int TimelineItem::GetNumSelected( void ) +{ + return m_nNumSelected; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::SnapAll() +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + for ( int t = 0; t < 2; t++ ) + { + for ( int i = 0; i < track->GetNumSamples( t ); i++ ) + { + CExpressionSample *sample = track->GetSample( i, t ); + sample->time = FacePoser_SnapTime( sample->time ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::SnapSelected() +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + for ( int t = 0; t < 2; t++ ) + { + for ( int i = 0; i < track->GetNumSamples( t ); i++ ) + { + CExpressionSample *sample = track->GetSample( i, t ); + if ( !sample->selected ) + continue; + + sample->time = FacePoser_SnapTime( sample->time ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : start - +// end - +//----------------------------------------------------------------------------- +void TimelineItem::DeletePoints( float start, float end ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + for ( int t = 0; t < 2; t++ ) + { + int num = track->GetNumSamples( t ); + for ( int i = num - 1; i >= 0 ; i-- ) + { + CExpressionSample *sample = track->GetSample( i, t ); + if ( sample->time < start || sample->time > end ) + continue; + + track->RemoveSample( i, t ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TimelineItem::OnDoubleClicked() +{ + // Disabled for now by request of BillF + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + SetCollapsed( !IsCollapsed() ); + g_pExpressionTool->LayoutItems( true ); +} + +void TimelineItem::DrawEventEnd( CChoreoWidgetDrawHelper& drawHelper ) +{ + CFlexAnimationTrack *track = GetSafeTrack(); + if ( !track ) + return; + + CChoreoEvent *e = track->GetEvent(); + if ( !e ) + return; + + float duration = e->GetDuration(); + if ( !duration ) + return; + + int leftx = GetMouseForTime( duration ); + if ( leftx > m_rcBounds.right ) + return; + + drawHelper.DrawColoredLine( + COLOR_CHOREO_ENDTIME, PS_SOLID, 1, + leftx, m_rcBounds.top, leftx, m_rcBounds.bottom ); + +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : helper - +// handleRect - +//----------------------------------------------------------------------------- +void TimelineItem::DrawGrowHandle( CChoreoWidgetDrawHelper& helper, RECT& handleRect ) +{ + Assert(CanHaveGrowHandle()); + + RECT useRect = handleRect; + helper.OffsetSubRect( useRect ); + + POINT region[4]; + int cPoints = 4; + + region[ 0 ].x = useRect.left + GROW_HANDLE_INSETPIXELS; + region[ 0 ].y = useRect.top; + + region[ 1 ].x = useRect.right - GROW_HANDLE_INSETPIXELS; + region[ 1 ].y = useRect.top; + + region[ 2 ].x = useRect.right; + region[ 2 ].y = useRect.bottom; + + region[ 3 ].x = useRect.left; + region[ 3 ].y = useRect.bottom; + + HDC dc = helper.GrabDC(); + + HRGN rgn = CreatePolygonRgn( region, cPoints, ALTERNATE ); + + int oldPF = SetPolyFillMode( dc, ALTERNATE ); + + HBRUSH brBg = CreateSolidBrush( RGB( 150, 150, 150 ) ); + HBRUSH brBorder = CreateSolidBrush( RGB( 200, 200, 200 ) ); + + FillRgn( dc, rgn, brBg ); + FrameRgn( dc, rgn, brBorder, 1, 1 ); + + SetPolyFillMode( dc, oldPF ); + + DeleteObject( rgn ); + + DeleteObject( brBg ); + DeleteObject( brBorder ); + + // draw a line in the middle + int midy = ( handleRect.bottom + handleRect.top ) * 0.5f; + int lineinset = GROW_HANDLE_INSETPIXELS *1.5; + + helper.DrawColoredLine( RGB( 63, 63, 63 ), PS_SOLID, 1, + handleRect.left + lineinset, midy, + handleRect.right - lineinset, midy ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : rc - +//----------------------------------------------------------------------------- +void TimelineItem::GetGrowHandleRect( RECT& rc ) +{ + rc = m_rcBounds; + rc.bottom -= 1; + rc.top = rc.bottom - GROW_HANDLE_HEIGHT; + rc.left = ( rc.right + rc.left ) / 2 - GROW_HANDLE_WIDTH / 2; + rc.right = rc.left + GROW_HANDLE_WIDTH; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool TimelineItem::CanHaveGrowHandle() +{ + if ( IsCollapsed() ) + return false; + + if ( !g_pExpressionTool->IsFocusItem( this ) ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : x - +// y - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool TimelineItem::IsMouseOverGrowHandle( int x, int y) +{ + if ( !CanHaveGrowHandle() ) + return false; + + RECT rcGrowHandle; + GetGrowHandleRect( rcGrowHandle ); + + POINT pt; + pt.x = x + m_rcBounds.left; + pt.y = y + m_rcBounds.top; + + return PtInRect( &rcGrowHandle, pt ) ? true : false; +} + +void TimelineItem::DrawGrowRect() +{ + RECT rcFocus = m_rcBounds; + rcFocus.bottom = m_rcBounds.top + m_nLastY; + OffsetRect( &rcFocus, -m_rcBounds.left, -m_rcBounds.top ); + + POINT offset; + offset.x = m_rcBounds.left; + offset.y = m_rcBounds.top; + ClientToScreen( (HWND)m_pWorkspace->getHandle(), &offset ); + OffsetRect( &rcFocus, offset.x, offset.y ); + + HDC dc = GetDC( NULL ); + + ::DrawFocusRect( dc, &rcFocus ); + + ReleaseDC( NULL, dc ); +} + +void TimelineItem::GetWorkList( bool reflect, CUtlVector< TimelineItem * >& list ) +{ + if ( !reflect ) + { + list.AddToTail( this ); + } + else + { + g_pExpressionTool->GetTimelineItems( list ); + } +} |