1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 TrackArtist.cpp
6
7 Dominic Mazzoni
8
9
10 *******************************************************************//*!
11
12 \class TrackArtist
13 \brief This class handles the actual rendering of WaveTracks (both
14 waveforms and spectra), NoteTracks, LabelTracks and TimeTracks.
15
16 It's actually a little harder than it looks, because for
17 waveforms at least it needs to cache the samples that are
18 currently on-screen.
19
20 <b>How Audacity Redisplay Works \n
21 Roger Dannenberg</b> \n
22 Oct 2010 \n
23
24 In my opinion, the bitmap should contain only the waveform, note, and
25 label images along with gray selection highlights. The track info
26 (sliders, buttons, title, etc.), track selection highlight, cursor, and
27 indicator should be drawn in the normal way, and clipping regions should
28 be used to avoid excessive copying of bitmaps (say, when sliders move),
29 or excessive redrawing of track info widgets (say, when scrolling occurs).
30 This is a fairly tricky code change since it requires careful specification
31 of what and where redraw should take place when any state changes. One
32 surprising finding is that NoteTrack display is slow compared to WaveTrack
33 display. Each note takes some time to gather attributes and select colors,
34 and while audio draws two amplitudes per horizontal pixels, large MIDI
35 scores can have more notes than horizontal pixels. This can make slider
36 changes very sluggish, but this can also be a problem with many
37 audio tracks.
38
39 *//*******************************************************************/
40
41
42 #include "TrackArtist.h"
43
44
45
46 #include "AColor.h"
47 #include "AllThemeResources.h"
48 #include "prefs/GUIPrefs.h"
49 #include "Theme.h"
50 #include "Track.h"
51 #include "TrackPanelDrawingContext.h"
52 #include "ViewInfo.h"
53
54 #include "Decibels.h"
55 #include "prefs/TracksPrefs.h"
56
57 #include <wx/app.h>
58 #include <wx/dc.h>
59
60 //Thickness of the clip frame outline, shown when clip is dragged
61 static constexpr int ClipSelectionStrokeSize{ 1 };//px
62
TrackArtist(TrackPanel * parent_)63 TrackArtist::TrackArtist( TrackPanel *parent_ )
64 : parent( parent_ )
65 {
66 mdBrange = DecibelScaleCutoff.GetDefault();
67 mShowClipping = false;
68 mSampleDisplay = 1;// Stem plots by default.
69
70 SetColours(0);
71
72 UpdatePrefs();
73 }
74
~TrackArtist()75 TrackArtist::~TrackArtist()
76 {
77 }
78
Get(TrackPanelDrawingContext & context)79 TrackArtist * TrackArtist::Get( TrackPanelDrawingContext &context )
80 {
81 return static_cast< TrackArtist* >( context.pUserData );
82 }
83
SetColours(int iColorIndex)84 void TrackArtist::SetColours( int iColorIndex)
85 {
86 theTheme.SetBrushColour( blankBrush, clrBlank );
87 theTheme.SetBrushColour( unselectedBrush, clrUnselected);
88 theTheme.SetBrushColour( selectedBrush, clrSelected);
89 theTheme.SetBrushColour( sampleBrush, clrSample);
90 theTheme.SetBrushColour( selsampleBrush, clrSelSample);
91 theTheme.SetBrushColour( dragsampleBrush, clrDragSample);
92 theTheme.SetBrushColour( blankSelectedBrush, clrBlankSelected);
93
94 theTheme.SetPenColour( blankPen, clrBlank);
95 theTheme.SetPenColour( unselectedPen, clrUnselected);
96 theTheme.SetPenColour( selectedPen, clrSelected);
97 theTheme.SetPenColour( muteSamplePen, clrMuteSample);
98 theTheme.SetPenColour( odProgressDonePen, clrProgressDone);
99 theTheme.SetPenColour( odProgressNotYetPen, clrProgressNotYet);
100 theTheme.SetPenColour( shadowPen, clrShadow);
101 theTheme.SetPenColour( clippedPen, clrClipped);
102 theTheme.SetPenColour( muteClippedPen, clrMuteClipped);
103 theTheme.SetPenColour( blankSelectedPen,clrBlankSelected);
104
105 theTheme.SetPenColour( selsamplePen, clrSelSample);
106 theTheme.SetPenColour( muteRmsPen, clrMuteRms);
107
108 switch( iColorIndex %4 )
109 {
110 default:
111 case 0:
112 theTheme.SetPenColour( samplePen, clrSample);
113 theTheme.SetPenColour( rmsPen, clrRms);
114 break;
115 case 1: // RED
116 samplePen.SetColour( wxColor( 160,10,10 ) );
117 rmsPen.SetColour( wxColor( 230,80,80 ) );
118 break;
119 case 2: // GREEN
120 samplePen.SetColour( wxColor( 35,110,35 ) );
121 rmsPen.SetColour( wxColor( 75,200,75 ) );
122 break;
123 case 3: //BLACK
124 samplePen.SetColour( wxColor( 0,0,0 ) );
125 rmsPen.SetColour( wxColor( 100,100,100 ) );
126 break;
127
128 }
129 }
130
131 /// Takes a value between min and max and returns a value between
132 /// height and 0
133 /// \todo Should this function move int GuiWaveTrack where it can
134 /// then use the zoomMin, zoomMax and height values without having
135 /// to have them passed in to it??
GetWaveYPos(float value,float min,float max,int height,bool dB,bool outer,float dBr,bool clip)136 int GetWaveYPos(float value, float min, float max,
137 int height, bool dB, bool outer,
138 float dBr, bool clip)
139 {
140 if (dB) {
141 if (height == 0) {
142 return 0;
143 }
144
145 float sign = (value >= 0 ? 1 : -1);
146
147 if (value != 0.) {
148 float db = LINEAR_TO_DB(fabs(value));
149 value = (db + dBr) / dBr;
150 if (!outer) {
151 value -= 0.5;
152 }
153 if (value < 0.0) {
154 value = 0.0;
155 }
156 value *= sign;
157 }
158 }
159 else {
160 if (!outer) {
161 if (value >= 0.0) {
162 value -= 0.5;
163 }
164 else {
165 value += 0.5;
166 }
167 }
168 }
169
170 if (clip) {
171 if (value < min) {
172 value = min;
173 }
174 if (value > max) {
175 value = max;
176 }
177 }
178
179 value = (max - value) / (max - min);
180 return (int) (value * (height - 1) + 0.5);
181 }
182
FromDB(float value,double dBRange)183 float FromDB(float value, double dBRange)
184 {
185 if (value == 0)
186 return 0;
187
188 double sign = (value >= 0 ? 1 : -1);
189 return DB_TO_LINEAR((fabs(value) * dBRange) - dBRange) * sign;
190 }
191
ValueOfPixel(int yy,int height,bool offset,bool dB,double dBRange,float zoomMin,float zoomMax)192 float ValueOfPixel(int yy, int height, bool offset,
193 bool dB, double dBRange, float zoomMin, float zoomMax)
194 {
195 wxASSERT(height > 0);
196 // Map 0 to max and height - 1 (not height) to min
197 float v =
198 height == 1 ? (zoomMin + zoomMax) / 2 :
199 zoomMax - (yy / (float)(height - 1)) * (zoomMax - zoomMin);
200 if (offset) {
201 if (v > 0.0)
202 v += .5;
203 else
204 v -= .5;
205 }
206
207 if (dB)
208 v = FromDB(v, dBRange);
209
210 return v;
211 }
212
DrawNegativeOffsetTrackArrows(TrackPanelDrawingContext & context,const wxRect & rect)213 void TrackArt::DrawNegativeOffsetTrackArrows(
214 TrackPanelDrawingContext &context, const wxRect &rect )
215 {
216 auto &dc = context.dc;
217
218 // Draws two black arrows on the left side of the track to
219 // indicate the user that the track has been time-shifted
220 // to the left beyond t=0.0.
221
222 dc.SetPen(*wxBLACK_PEN);
223 AColor::Line(dc,
224 rect.x + 2, rect.y + 6,
225 rect.x + 8, rect.y + 6);
226 AColor::Line(dc,
227 rect.x + 2, rect.y + 6,
228 rect.x + 6, rect.y + 2);
229 AColor::Line(dc,
230 rect.x + 2, rect.y + 6,
231 rect.x + 6, rect.y + 10);
232 AColor::Line(dc,
233 rect.x + 2, rect.y + rect.height - 8,
234 rect.x + 8, rect.y + rect.height - 8);
235 AColor::Line(dc,
236 rect.x + 2, rect.y + rect.height - 8,
237 rect.x + 6, rect.y + rect.height - 4);
238 AColor::Line(dc,
239 rect.x + 2, rect.y + rect.height - 8,
240 rect.x + 6, rect.y + rect.height - 12);
241 }
242
TruncateText(wxDC & dc,const wxString & text,const int maxWidth)243 wxString TrackArt::TruncateText(wxDC& dc, const wxString& text, const int maxWidth)
244 {
245 static const wxString ellipsis = "\u2026";
246
247 if (dc.GetTextExtent(text).GetWidth() <= maxWidth)
248 return text;
249
250 auto left = 0;
251 //no need to check text + '...'
252 auto right = static_cast<int>(text.Length() - 2);
253
254 while (left <= right)
255 {
256 auto middle = (left + right) / 2;
257 auto str = text.SubString(0, middle).Trim() + ellipsis;
258 auto strWidth = dc.GetTextExtent(str).GetWidth();
259 if (strWidth < maxWidth)
260 //if left == right (== middle), then exit loop
261 //with right equals to the last knwon index for which
262 //strWidth < maxWidth
263 left = middle + 1;
264 else if (strWidth > maxWidth)
265 //if right == left (== middle), then exit loop with
266 //right equals to (left - 1), which is the last known
267 //index for which (strWidth < maxWidth) or -1
268 right = middle - 1;
269 else
270 return str;
271 }
272 if (right >= 0)
273 return text.SubString(0, right).Trim() + ellipsis;
274
275 return wxEmptyString;
276 }
277
278
279 #ifdef USE_MIDI
280 #endif // USE_MIDI
281
282
UpdateSelectedPrefs(int id)283 void TrackArtist::UpdateSelectedPrefs( int id )
284 {
285 if( id == ShowClippingPrefsID())
286 mShowClipping = gPrefs->Read(wxT("/GUI/ShowClipping"), mShowClipping);
287 if( id == ShowTrackNameInWaveformPrefsID())
288 mbShowTrackNameInTrack = gPrefs->ReadBool(wxT("/GUI/ShowTrackNameInWaveform"), false);
289 }
290
UpdatePrefs()291 void TrackArtist::UpdatePrefs()
292 {
293 mdBrange = DecibelScaleCutoff.Read();
294 mSampleDisplay = TracksPrefs::SampleViewChoice();
295
296 UpdateSelectedPrefs( ShowClippingPrefsID() );
297 UpdateSelectedPrefs( ShowTrackNameInWaveformPrefsID() );
298
299 SetColours(0);
300 }
301
DrawClipAffordance(wxDC & dc,const wxRect & rect,const wxString & title,bool highlight,bool selected)302 void TrackArt::DrawClipAffordance(wxDC& dc, const wxRect& rect, const wxString& title, bool highlight, bool selected)
303 {
304 //To make sure that roundings do not overlap each other
305 auto clipFrameRadius = std::min(ClipFrameRadius, rect.width / 2);
306
307 wxRect clipRect;
308 bool hasClipRect = dc.GetClippingBox(clipRect);
309 //Fix #1689: visual glitches appear on attempt to draw a rectangle
310 //larger than 0x7FFFFFF pixels wide (value was discovered
311 //by manual testing, and maybe depends on OS being used), but
312 //it's very unlikely that such huge rectangle will be ever fully visible
313 //on the screen, so we can safely reduce its size to be slightly larger than
314 //clipping rectangle, and avoid that problem
315 auto drawingRect = rect;
316 if (hasClipRect)
317 {
318 //to make sure that rounding happends outside the clipping rectangle
319 drawingRect.SetLeft(std::max(rect.GetLeft(), clipRect.GetLeft() - clipFrameRadius - 1));
320 drawingRect.SetRight(std::min(rect.GetRight(), clipRect.GetRight() + clipFrameRadius + 1));
321 }
322
323 if (selected)
324 {
325 wxRect strokeRect{
326 drawingRect.x - ClipSelectionStrokeSize,
327 drawingRect.y,
328 drawingRect.width + ClipSelectionStrokeSize * 2,
329 drawingRect.height + clipFrameRadius };
330 dc.SetBrush(*wxTRANSPARENT_BRUSH);
331 AColor::UseThemeColour(&dc, clrClipAffordanceStroke, clrClipAffordanceStroke);
332 dc.DrawRoundedRectangle(strokeRect, clipFrameRadius);
333 }
334
335 AColor::UseThemeColour(&dc, highlight ? clrClipAffordanceActiveBrush : clrClipAffordanceInactiveBrush, clrClipAffordanceOutlinePen);
336 dc.DrawRoundedRectangle(
337 wxRect(
338 drawingRect.x,
339 drawingRect.y + ClipSelectionStrokeSize,
340 drawingRect.width,
341 drawingRect.height + clipFrameRadius
342 ), clipFrameRadius
343 );
344
345 if (!title.empty())
346 {
347 auto titleRect = hasClipRect ?
348 //avoid drawing text outside the clipping rectangle if possible
349 TrackArt::GetAffordanceTitleRect(rect.Intersect(clipRect)) :
350 TrackArt::GetAffordanceTitleRect(rect);
351
352 auto truncatedTitle = TrackArt::TruncateText(dc, title, titleRect.GetWidth());
353 if (!truncatedTitle.empty())
354 {
355 auto hAlign = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? wxALIGN_RIGHT : wxALIGN_LEFT;
356 dc.DrawLabel(truncatedTitle, titleRect, hAlign | wxALIGN_CENTER_VERTICAL);
357 }
358 }
359 }
360
GetAffordanceTitleRect(const wxRect & rect)361 AUDACITY_DLL_API wxRect TrackArt::GetAffordanceTitleRect(const wxRect& rect)
362 {
363 constexpr int FrameThickness{ 1 };
364 return wxRect(
365 rect.GetLeft() + ClipFrameRadius,
366 rect.GetTop() + ClipSelectionStrokeSize + FrameThickness,
367 rect.GetWidth() - ClipFrameRadius * 2,
368 rect.GetHeight() - ClipSelectionStrokeSize - FrameThickness);
369 }
370
DrawClipEdges(wxDC & dc,const wxRect & clipRect,bool selected)371 void TrackArt::DrawClipEdges(wxDC& dc, const wxRect& clipRect, bool selected)
372 {
373 dc.SetBrush(*wxTRANSPARENT_BRUSH);
374 {
375 AColor::UseThemeColour(&dc, -1, clrClipAffordanceOutlinePen);
376 AColor::Line(dc,
377 clipRect.GetLeft(), clipRect.GetTop(),
378 clipRect.GetLeft(), clipRect.GetBottom());
379 AColor::Line(dc,
380 clipRect.GetRight(), clipRect.GetTop(),
381 clipRect.GetRight(), clipRect.GetBottom());
382 }
383 if(selected)
384 {
385 if constexpr (ClipSelectionStrokeSize == 1)
386 {
387 AColor::UseThemeColour(&dc, -1, clrClipAffordanceStroke);
388 AColor::Line(dc,
389 clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetTop(),
390 clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetBottom());
391 AColor::Line(dc,
392 clipRect.GetRight() + ClipSelectionStrokeSize, clipRect.GetTop(),
393 clipRect.GetRight() + ClipSelectionStrokeSize, clipRect.GetBottom());
394 }
395 else if constexpr (ClipSelectionStrokeSize > 1)
396 {
397 AColor::UseThemeColour(&dc, clrClipAffordanceStroke, clrClipAffordanceStroke);
398 dc.DrawRectangle(wxRect(
399 clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetTop(),
400 ClipSelectionStrokeSize, clipRect.GetHeight()));
401 dc.DrawRectangle(wxRect(
402 clipRect.GetRight() + 1, clipRect.GetTop(),
403 ClipSelectionStrokeSize, clipRect.GetHeight()));
404 }
405 }
406 }
407
DrawClipFolded(wxDC & dc,const wxRect & rect)408 void TrackArt::DrawClipFolded(wxDC& dc, const wxRect& rect)
409 {
410 AColor::UseThemeColour(&dc, clrClipAffordanceOutlinePen);
411 dc.DrawRectangle(rect);
412 }
413
414 // Draws the sync-lock bitmap, tiled; always draws stationary relative to the DC
415 //
416 // AWD: now that the tiles don't link together, we're drawing a tilted grid, at
417 // two steps down for every one across. This creates a pattern that repeats in
418 // 5-step by 5-step boxes. Because we're only drawing in 5/25 possible positions
419 // we have a grid spacing somewhat smaller than the image dimensions. Thus we
420 // achieve lower density than with a square grid and eliminate edge cases where
421 // no tiles are displayed.
422 //
423 // The pattern draws in tiles at (0,0), (2,1), (4,2), (1,3), and (3,4) in each
424 // 5x5 box.
425 //
426 // There may be a better way to do this, or a more appealing pattern.
DrawSyncLockTiles(TrackPanelDrawingContext & context,const wxRect & rect)427 void TrackArt::DrawSyncLockTiles(
428 TrackPanelDrawingContext &context, const wxRect &rect )
429 {
430 const auto dc = &context.dc;
431
432 wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockSelTile));
433
434 // Grid spacing is a bit smaller than actual image size
435 int gridW = syncLockBitmap.GetWidth() - 6;
436 int gridH = syncLockBitmap.GetHeight() - 8;
437
438 // Horizontal position within the grid, modulo its period
439 int blockX = (rect.x / gridW) % 5;
440
441 // Amount to offset drawing of first column
442 int xOffset = rect.x % gridW;
443 if (xOffset < 0) xOffset += gridW;
444
445 // Check if we're missing an extra column to the left (this can happen
446 // because the tiles are bigger than the grid spacing)
447 bool extraCol = false;
448 if (syncLockBitmap.GetWidth() - gridW > xOffset) {
449 extraCol = true;
450 xOffset += gridW;
451 blockX = (blockX - 1) % 5;
452 }
453 // Make sure blockX is non-negative
454 if (blockX < 0) blockX += 5;
455
456 int xx = 0;
457 while (xx < rect.width) {
458 int width = syncLockBitmap.GetWidth() - xOffset;
459 if (xx + width > rect.width)
460 width = rect.width - xx;
461
462 //
463 // Draw each row in this column
464 //
465
466 // Vertical position in the grid, modulo its period
467 int blockY = (rect.y / gridH) % 5;
468
469 // Amount to offset drawing of first row
470 int yOffset = rect.y % gridH;
471 if (yOffset < 0) yOffset += gridH;
472
473 // Check if we're missing an extra row on top (this can happen because
474 // the tiles are bigger than the grid spacing)
475 bool extraRow = false;
476 if (syncLockBitmap.GetHeight() - gridH > yOffset) {
477 extraRow = true;
478 yOffset += gridH;
479 blockY = (blockY - 1) % 5;
480 }
481 // Make sure blockY is non-negative
482 if (blockY < 0) blockY += 5;
483
484 int yy = 0;
485 while (yy < rect.height)
486 {
487 int height = syncLockBitmap.GetHeight() - yOffset;
488 if (yy + height > rect.height)
489 height = rect.height - yy;
490
491 // AWD: draw blocks according to our pattern
492 if ((blockX == 0 && blockY == 0) || (blockX == 2 && blockY == 1) ||
493 (blockX == 4 && blockY == 2) || (blockX == 1 && blockY == 3) ||
494 (blockX == 3 && blockY == 4))
495 {
496
497 // Do we need to get a sub-bitmap?
498 if (width != syncLockBitmap.GetWidth() || height != syncLockBitmap.GetHeight()) {
499 wxBitmap subSyncLockBitmap =
500 syncLockBitmap.GetSubBitmap(wxRect(xOffset, yOffset, width, height));
501 dc->DrawBitmap(subSyncLockBitmap, rect.x + xx, rect.y + yy, true);
502 }
503 else {
504 dc->DrawBitmap(syncLockBitmap, rect.x + xx, rect.y + yy, true);
505 }
506 }
507
508 // Updates for next row
509 if (extraRow) {
510 // Second offset row, still at y = 0; no more extra rows
511 yOffset -= gridH;
512 extraRow = false;
513 }
514 else {
515 // Move on in y, no more offset rows
516 yy += gridH - yOffset;
517 yOffset = 0;
518 }
519 blockY = (blockY + 1) % 5;
520 }
521
522 // Updates for next column
523 if (extraCol) {
524 // Second offset column, still at x = 0; no more extra columns
525 xOffset -= gridW;
526 extraCol = false;
527 }
528 else {
529 // Move on in x, no more offset rows
530 xx += gridW - xOffset;
531 xOffset = 0;
532 }
533 blockX = (blockX + 1) % 5;
534 }
535 }
536
DrawBackgroundWithSelection(TrackPanelDrawingContext & context,const wxRect & rect,const Track * track,const wxBrush & selBrush,const wxBrush & unselBrush,bool useSelection)537 void TrackArt::DrawBackgroundWithSelection(
538 TrackPanelDrawingContext &context, const wxRect &rect,
539 const Track *track, const wxBrush &selBrush, const wxBrush &unselBrush,
540 bool useSelection)
541 {
542 const auto dc = &context.dc;
543 const auto artist = TrackArtist::Get( context );
544 const auto &selectedRegion = *artist->pSelectedRegion;
545 const auto &zoomInfo = *artist->pZoomInfo;
546
547 //MM: Draw background. We should optimize that a bit more.
548 const double sel0 = useSelection ? selectedRegion.t0() : 0.0;
549 const double sel1 = useSelection ? selectedRegion.t1() : 0.0;
550
551 dc->SetPen(*wxTRANSPARENT_PEN);
552 if (track->GetSelected() || track->IsSyncLockSelected())
553 {
554 // Rectangles before, within, after the selection
555 wxRect before = rect;
556 wxRect within = rect;
557 wxRect after = rect;
558
559 before.width = (int)(zoomInfo.TimeToPosition(sel0) );
560 if (before.GetRight() > rect.GetRight()) {
561 before.width = rect.width;
562 }
563
564 if (before.width > 0) {
565 dc->SetBrush(unselBrush);
566 dc->DrawRectangle(before);
567
568 within.x = 1 + before.GetRight();
569 }
570 within.width = rect.x + (int)(zoomInfo.TimeToPosition(sel1) ) - within.x -1;
571
572 if (within.GetRight() > rect.GetRight()) {
573 within.width = 1 + rect.GetRight() - within.x;
574 }
575
576 // Bug 2389 - Selection can disappear
577 // This handles case where no waveform is visible.
578 if (within.width < 1)
579 {
580 within.width = 1;
581 }
582
583 if (within.width > 0) {
584 if (track->GetSelected()) {
585 dc->SetBrush(selBrush);
586 dc->DrawRectangle(within);
587 }
588 else {
589 // Per condition above, track must be sync-lock selected
590 dc->SetBrush(unselBrush);
591 dc->DrawRectangle(within);
592 DrawSyncLockTiles( context, within );
593 }
594
595 after.x = 1 + within.GetRight();
596 }
597 else {
598 // `within` not drawn; start where it would have gone
599 after.x = within.x;
600 }
601
602 after.width = 1 + rect.GetRight() - after.x;
603 if (after.width > 0) {
604 dc->SetBrush(unselBrush);
605 dc->DrawRectangle(after);
606 }
607 }
608 else
609 {
610 // Track not selected; just draw background
611 dc->SetBrush(unselBrush);
612 dc->DrawRectangle(rect);
613 }
614 }
615
DrawCursor(TrackPanelDrawingContext & context,const wxRect & rect,const Track * track)616 void TrackArt::DrawCursor(TrackPanelDrawingContext& context,
617 const wxRect& rect, const Track* track)
618 {
619 const auto dc = &context.dc;
620 const auto artist = TrackArtist::Get(context);
621 const auto& selectedRegion = *artist->pSelectedRegion;
622
623 if (selectedRegion.isPoint())
624 {
625 const auto& zoomInfo = *artist->pZoomInfo;
626 auto x = static_cast<int>(zoomInfo.TimeToPosition(selectedRegion.t0(), rect.x));
627 if (x >= rect.GetLeft() && x <= rect.GetRight())
628 {
629 AColor::CursorColor(dc);
630 AColor::Line(*dc, x, rect.GetTop(), x, rect.GetBottom());
631 }
632 }
633 }
634
635