Lumiera  0.pre.03
»edit your freedom«
zoom-window-test.cpp
Go to the documentation of this file.
1 /*
2  ZoomWindow(Test) - verify translation from model to screen coordinates
3 
4  Copyright (C)
5  2018, Hermann Vosseler <Ichthyostega@web.de>
6 
7   **Lumiera** is free software; you can redistribute it and/or modify it
8   under the terms of the GNU General Public License as published by the
9   Free Software Foundation; either version 2 of the License, or (at your
10   option) any later version. See the file COPYING for further details.
11 
12 * *****************************************************************/
13 
61 #include "lib/test/run.hpp"
62 #include "lib/test/test-helper.hpp"
64 
65 
66 namespace stage{
67 namespace model{
68 namespace test {
69 
70 
71  namespace { // simplified notation for expected results...
72 
73  inline Time _t(int secs) { return Time(FSecs(secs)); }
74  inline Time _t(int s, int div) { return Time(FSecs(s,div)); }
75  }
76 
77 
78 
79 
80  /*************************************************************************************/
91  class ZoomWindow_test : public Test
92  {
93 
94  virtual void
95  run (Arg)
96  {
97  // Explanation of the notation used in this test...
98  CHECK (_t(10) == Time{FSecs(10)}); // Time point at t = 10sec
99  CHECK (_t(10,3) == Time{FSecs(10,3)}); // Time point at t = 10/3sec (fractional number)
100  CHECK (FSecs(10,3) == FSecs(10)/3); // fractional number arithmetics
101  CHECK (FSecs(10)/3 == 10_r/3); // _r is a user defined literal to denote 64-bit fractional
102  CHECK (Rat(10,3) == 10_r/3);
103  CHECK (Rat(10,3) == boost::rational<int64_t>(10,3)); // using Rat = boost::rational<int64_t>
104  CHECK (rational_cast<float> (10_r/3) == 3.3333333f); // rational_cast calculates division after type conversion
105 
107  verify_setup();
109  verify_metric();
110  verify_window();
111  verify_scroll();
112 
114 
124  }
125 
126 
128  void
130  {
131  ZoomWindow zoomWin;
132  CHECK (zoomWin.overallSpan() == TimeSpan(_t(0), FSecs(23)));
133  CHECK (zoomWin.visible() == TimeSpan(_t(0), FSecs(23)));
134  CHECK (zoomWin.px_per_sec() == 25);
135 
136  zoomWin.nudgeMetric(+1);
137  CHECK (zoomWin.px_per_sec() == 50);
138  CHECK (zoomWin.visible() == TimeSpan(_t(23,4), FSecs(23,2)));
139  CHECK (zoomWin.overallSpan() == TimeSpan(_t(0), FSecs(23)));
140 
141  zoomWin.nudgeVisiblePos(-1);
142  CHECK (zoomWin.px_per_sec() == 50);
143  CHECK (zoomWin.visible() == TimeSpan(_t(0), FSecs(23,2)));
144  CHECK (zoomWin.overallSpan() == TimeSpan(_t(0), FSecs(23)));
145  }
146 
147 
155  void
157  {
158  ZoomWindow win1;
159  CHECK (win1.overallSpan() == TimeSpan(_t(0), FSecs(23)));
160  CHECK (win1.visible() == win1.overallSpan());
161  CHECK (win1.pxWidth() == 25*23);
162  CHECK (win1.px_per_sec() == 25);
163 
164  ZoomWindow win2{TimeSpan{_t(-1), _t(+1)}};
165  CHECK (win2.overallSpan() == TimeSpan(_t(-1), FSecs(2)));
166  CHECK (win2.visible() == win2.overallSpan());
167  CHECK (win2.pxWidth() == 25*2);
168  CHECK (win2.px_per_sec() == 25);
169 
170  ZoomWindow win3{555};
171  CHECK (win3.overallSpan() == TimeSpan(_t(0), FSecs(23)));
172  CHECK (win3.visible() == win3.overallSpan());
173  CHECK (win3.pxWidth() == 555);
174  CHECK (win3.px_per_sec() == 555_r/23);
175 
176  ZoomWindow win4{555, TimeSpan{_t(-10), _t(-5)}};
177  CHECK (win4.overallSpan() == TimeSpan(-Time(0,10), FSecs(5)));
178  CHECK (win4.visible() == win4.overallSpan());
179  CHECK (win4.pxWidth() == 555);
180  CHECK (win4.px_per_sec() == 111);
181  }
182 
183 
192  void
194  {
195  ZoomWindow win;
196  CHECK (win.overallSpan() == TimeSpan(_t(0), FSecs(23)));
197  CHECK (win.visible() == TimeSpan(_t(0), FSecs(23)));
198  CHECK (win.pxWidth() == 23*25);
199 
200  win.calibrateExtension(25);
201  CHECK (win.overallSpan() == TimeSpan(_t(0), FSecs(23)));
202  CHECK (win.visible() == TimeSpan(_t(0), FSecs(1)));
203  CHECK (win.px_per_sec() == 25);
204  CHECK (win.pxWidth() == 25);
205 
206  win.setOverallRange(TimeSpan{_t(-50), _t(50)});
207  CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(100)));
208  CHECK (win.visible() == TimeSpan(_t(0), FSecs(1)));
209  CHECK (win.px_per_sec() == 25);
210  CHECK (win.pxWidth() == 25);
211 
212  win.calibrateExtension(100);
213  CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(100)));
214  CHECK (win.visible() == TimeSpan(_t(0), FSecs(4)));
215  CHECK (win.px_per_sec() == 25);
216  CHECK (win.pxWidth() == 100);
217 
218  win.setRanges (TimeSpan{_t(-50), _t(10)}, TimeSpan{_t(-10), FSecs(10)});
219  CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(60)));
220  CHECK (win.visible() == TimeSpan(_t(-10), _t(0)));
221  CHECK (win.px_per_sec() == 10);
222  CHECK (win.pxWidth() == 100);
223 
224  win.calibrateExtension(500);
225  CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(60)));
226  CHECK (win.visible() == TimeSpan(_t(-40), FSecs(50)));
227  CHECK (win.px_per_sec() == 10);
228  CHECK (win.pxWidth() == 500);
229 
230  win.setOverallDuration (Duration{FSecs(30)});
231  CHECK (win.overallSpan() == TimeSpan(_t(-50), _t(-20)));
232  CHECK (win.visible() == TimeSpan(_t(-50), FSecs(30)));
233  CHECK (win.px_per_sec() == 500_r/30);
234  CHECK (win.pxWidth() == 500);
235 
236  win.calibrateExtension(300);
237  CHECK (win.overallSpan() == TimeSpan(_t(-50), _t(-20)));
238  CHECK (win.visible() == TimeSpan(_t(-50), FSecs(30)*3/5));
239  CHECK (win.px_per_sec() == 500_r/30);
240  CHECK (win.pxWidth() == 300);
241  }
242 
243 
250  void
252  {
253  ZoomWindow win{1280, TimeSpan{_t(0), FSecs(64)}};
254  CHECK (win.px_per_sec() == 20);
255 
256  win.nudgeMetric(+1);
257  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
258  CHECK (win.visible() == TimeSpan(_t(32,2), FSecs(32)));
259  CHECK (win.px_per_sec() == 40);
260  CHECK (win.pxWidth() == 1280);
261 
262  win.setVisiblePos(0.0);
263  CHECK (win.visible() == TimeSpan(_t(0), FSecs(32))); // zoom window moved to left side of overall range
264 
265  win.nudgeMetric(+15);
266  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
267  CHECK (win.visible() == TimeSpan(_t(0), FSecs(32,32768) +MICRO_TICK));
268  CHECK (win.visible().start() == _t(0)); // now anchor position is at left bound
269  CHECK (win.visible().end() == TimeValue(977)); // length was rounded up to the next grid position
270  CHECK (Time{FSecs(32,32768)+MICRO_TICK} == TimeValue(977)); // (preferring slightly larger window unless perfect fit)
271  CHECK (Time{FSecs(32,32768) } == TimeValue(976));
272  // scale factor calculated back from actual window width
273  CHECK (win.px_per_sec() == 1280_r/977 * Time::SCALE);
274  CHECK (win.pxWidth() == 1280);
275  // Note: already getting close to the time grid...
276  CHECK (Time(FSecs(32,32768)) == TimeValue(976));
277  CHECK (rational_cast<double> (32_r/32768 * Time::SCALE) == 976.5625);
278 
279  win.nudgeMetric(+1);
280  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
281  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION); // further zoom has been capped at 2px per µ-tick
282  CHECK (win.visible() == TimeSpan(_t(0), FSecs(1280_r/ZOOM_MAX_RESOLUTION)));
283  CHECK (win.pxWidth() == 1280);
284 
285  win.nudgeMetric(+1);
286  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION);
287  win.setMetric(10*ZOOM_MAX_RESOLUTION);
288  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION);
289 
290  // so this is the deepest zoom possible....
291  CHECK (win.visible().duration() == TimeValue(640));
292  CHECK (TimeValue{640} == Time{Rat(1280)/ZOOM_MAX_RESOLUTION});
293 
294  // and this the absolutely smallest possible zoom window
295  win.calibrateExtension(2);
296  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
297  CHECK (win.visible().duration() == TimeValue(1));
298  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION);
299  CHECK (win.pxWidth() == 2);
300 
301  win.calibrateExtension(1);
302  CHECK (win.visible().duration() == TimeValue(1)); // window is guaranteed to be non-empty
303  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION / 2); // zoom scale has thus been lowered to prevent window from vanishing
304  CHECK (win.pxWidth() == 1);
305 
306  win.calibrateExtension(1280);
307  CHECK (win.visible().duration() == TimeValue(1280));
308  CHECK (win.visible().duration() == Duration{1280*MICRO_TICK});
309  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION / 2);
310  CHECK (win.pxWidth() == 1280);
311  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
312 
313  win.nudgeMetric(-5);
314  CHECK (win.visible().duration() == Duration{32 * 1280*MICRO_TICK});
315  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION / 64);
316  CHECK (win.pxWidth() == 1280);
317  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
318 
319  win.nudgeMetric(-12);
320  CHECK (win.visible() == win.overallSpan()); // zoom out stops at full canvas size
321  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
322  CHECK (win.px_per_sec() == 20);
323  CHECK (win.pxWidth() == 1280);
324 
325  // but canvas can be forcibly extended by »reverse zooming«
326  win.expandVisibleRange (TimeSpan{_t(60), _t(62)}); // zoom such as to bring current window at given relative position
327  CHECK (win.px_per_sec() == 20_r/64*2); // scale thus adjusted to reduce 64 sec to 2 sec (scale can be fractional!)
328  CHECK (win.visible().duration() == _t(64 * 32)); // zoom window has been inversely expanded by factor 64/2 == 32
329  CHECK (win.visible() == win.overallSpan()); // zoom fully covers the expanded canvas
330  CHECK (win.overallSpan() == TimeSpan(_t(-1920), _t(128))); // and overall canvas has been expanded to embed the previous window
331  CHECK (win.overallSpan().duration() == _t(2048)); // ... at indicated relative position (2sec ⟼ 64sec, one window size before end)
332 
333  // metric can be explicitly set (e.g. 5px per sound sample)
334  win.setMetric (5 / (1_r/44100));
335  CHECK (win.pxWidth() == 1280);
336  CHECK (win.px_per_sec() <= 5*44100); // zoom scale was slightly reduced to match exact pixel width
337  CHECK (win.px_per_sec() >= 5*44100 - 1);
338  CHECK (win.visible().duration() == Duration{1280_r/(5*44100) +MICRO_TICK});
339  CHECK (win.visible().duration() == Duration{1280_r/win.px_per_sec()});
340  CHECK (win.overallSpan().duration() == _t(2048));
341  }
342 
343 
345  void
347  {
348  ZoomWindow win{1280, TimeSpan{_t(0), FSecs(64)}};
349  CHECK (win.visible() == win.overallSpan());
350  CHECK (win.px_per_sec() == 20);
351 
352  win.setVisibleDuration (Duration{FSecs(23,30)});
353  CHECK (win.visible().duration() == _t(23,30));
354  CHECK (win.visible().start() == _t(64,2) - _t(23,30*2)); // when zooming down from full range, zoom anchor is window centre
355  CHECK (win.px_per_sec() == 1280_r/_FSecs(_t(23,30))); // scale factor slightly adjusted to match exact pixel width
356  CHECK (win.pxWidth() == 1280);
357 
358  win.setVisibleRange (TimeSpan{_t(12), FSecs(16)});
359  CHECK (win.visible() == TimeSpan(_t(12), _t(12+16)));
360  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
361  CHECK (win.px_per_sec() == 1280_r/16);
362  CHECK (win.pxWidth() == 1280);
363 
364  win.setVisiblePos(_t(12)); // bring a specific position into sight
365  CHECK (win.visible().start() < _t(12)); // window is placed such as to enclose this desired position
366  CHECK (win.visible().duration() == _t(16)); // window size and metric not changed
367  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64)));
368  CHECK (win.px_per_sec() == 1280_r/16);
369  CHECK (win.pxWidth() == 1280);
370 
371  win.setVisiblePos(0.80); // positioning relatively within overall canvas
372  CHECK (win.visible().start() < Time{FSecs(64)*8/10}); // window will enclose the desired anchor position
373  CHECK (win.visible().end() > Time{FSecs(64)*8/10});
374  CHECK (win.px_per_sec() == 1280_r/16);
375  CHECK (win.pxWidth() == 1280);
376 
377  // manipulate canvas extension explicitly
378  win.setOverallDuration (Duration{FSecs(3600)});
379  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(3600)));
380  CHECK (win.px_per_sec() == 1280_r/16);
381  CHECK (win.pxWidth() == 1280);
382  CHECK (win.visible().duration() == _t(16)); // window position and size not affected
383  CHECK (win.visible().start() < Time{FSecs(64)*8/10});
384  CHECK (win.visible().end() > Time{FSecs(64)*8/10});
385 
386  // reposition nominal canvas anchoring
387  win.setOverallRange (TimeSpan{_t(-64), _t(-32)});
388  CHECK (win.overallSpan() == TimeSpan(_t(-64), FSecs(32))); // canvas nominally covers a completely different time range now
389  CHECK (win.px_per_sec() == 1280_r/16); // metric is retained
390  CHECK (win.pxWidth() == 1280);
391  CHECK (win.visible() == TimeSpan(_t(-32-16), FSecs(16))); // window scrolled left to remain within canvas
392 
393  win.setOverallStart (_t(100));
394  CHECK (win.overallSpan() == TimeSpan(_t(100), FSecs(32)));
395  CHECK (win.visible() == TimeSpan(_t(100), FSecs(16))); // window scrolled right to remain within canvas
396  CHECK (win.px_per_sec() == 1280_r/16); // metric is retained
397 
398  win.setOverallRange (TimeSpan{_t(50), _t(52)});
399  CHECK (win.overallSpan() == TimeSpan(_t(50), FSecs(2)));
400  CHECK (win.visible() == TimeSpan(_t(50), FSecs(2))); // window truncated to fit into canvas
401  CHECK (win.px_per_sec() == 1280_r/2); // metric need to be adjusted
402  CHECK (win.pxWidth() == 1280);
403  }
404 
405 
407  void
409  {
410  ZoomWindow win{1280, TimeSpan{_t(0), FSecs(16)}};
411  CHECK (win.visible() == win.overallSpan());
412  CHECK (win.visible() == TimeSpan(_t(0), FSecs(16)));
413  CHECK (win.px_per_sec() == 80);
414 
415  win.nudgeVisiblePos(+1);
416  CHECK (win.visible() == TimeSpan(_t(8), FSecs(16))); // window shifted forward by half a page
417  CHECK (win.overallSpan() == TimeSpan(_t(0), FSecs(16+8))); // canvas expanded accordingly
418  CHECK (win.px_per_sec() == 80); // metric is retained
419  CHECK (win.pxWidth() == 1280);
420 
421  win.nudgeVisiblePos(-3);
422  CHECK (win.visible() == TimeSpan(_t(-16), FSecs(16))); // window shifted backwards by three times half window size
423  CHECK (win.overallSpan() == TimeSpan(_t(-16), FSecs(16+8+16))); // canvas is always expanded accordingly, never shrinked
424  CHECK (win.px_per_sec() == 80); // metric is retained
425  CHECK (win.pxWidth() == 1280);
426 
427  win.setVisiblePos(0.50);
428  CHECK (win.visible() == TimeSpan(_t((40/2-16) -8), FSecs(16))); // window positioned to centre of canvas
429  CHECK (win.visible().start() == _t(-4)); // (canvas was already positioned asymmetrically)
430 
431  win.setVisiblePos(-0.50);
432  CHECK (win.visible() == TimeSpan(_t(-16-40/2), FSecs(16))); // relative positioning not limited at lower bound
433  CHECK (win.visible().start() == _t(-36)); // (causing also further expansion of canvas)
434  win.setVisiblePos(_t(200)); // absolute positioning likewise not limited
435  CHECK (win.visible() == TimeSpan(_t(200-16), FSecs(16))); // but anchored according to relative anchor pos
436  CHECK (win.px_per_sec() == 80); // metric retained
437  CHECK (win.pxWidth() == 1280);
438 
439  win.setVisibleRange(TimeSpan{_t(-200), FSecs(32)}); // but explicit positioning outside of canvas is possible
440  CHECK (win.overallSpan() == TimeSpan(_t(-200), _t(200))); // ...and will expand canvas
441  CHECK (win.visible() == TimeSpan(_t(-200), FSecs(32)));
442  CHECK (win.px_per_sec() == 40);
443  CHECK (win.pxWidth() == 1280);
444  }
445 
446 
448  void
450  {
451  ZoomWindow win{100, TimeSpan{_t(0), FSecs(4)}};
452  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(4)));
453  CHECK (win.visible() == TimeSpan(_t(0), _t(4)));
454  CHECK (win.px_per_sec() == 25);
455  CHECK (win.pxWidth() == 100);
456 
457  bool notified{false};
458  win.nudgeMetric(+1);
459  CHECK (not notified);
460  CHECK (win.px_per_sec() == 50);
461  CHECK (win.visible().duration() == _t(2));
462 
463  win.attachChangeNotification([&](){ notified = true; });
464  CHECK (not notified);
465  CHECK (win.px_per_sec() == 50);
466  win.nudgeMetric(+1);
467  CHECK (win.px_per_sec() == 100);
468  CHECK (notified);
469 
470  notified = false;
471  CHECK (win.visible().start() == _t(3,2));
472  win.nudgeVisiblePos(+1);
473  CHECK (win.visible().start() == _t(2));
474  CHECK (notified);
475 
476  notified = false;
477  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(4)));
478  win.setOverallRange(TimeSpan(_t(-4), _t(4)));
479  CHECK (win.overallSpan() == TimeSpan(_t(-4), _t(4)));
480  CHECK (notified);
481 
482  notified = false;
483  CHECK (win.pxWidth() == 100);
484  win.calibrateExtension(200);
485  CHECK (win.pxWidth() == 200);
486  CHECK (win.px_per_sec() == 100);
487  CHECK (notified);
488 
489  notified = false;
490  bool otherTriger{false};
491  ZoomWindow wuz{10, TimeSpan{_t(0), FSecs(1)}};
492  wuz.attachChangeNotification([&](){ otherTriger = true; });
493  CHECK (wuz.visible().start() == _t(0));
494  CHECK (not notified);
495  CHECK (not otherTriger);
496  wuz.nudgeVisiblePos(-1);
497  CHECK (not notified);
498  CHECK (otherTriger);
499  CHECK (wuz.visible().start() == _t(-1,2));
500 
501  otherTriger = false;
502  CHECK (not notified);
503  win.nudgeMetric(+1);
504  CHECK (not otherTriger);
505  CHECK (notified);
506  CHECK (win.px_per_sec() == 200);
507  CHECK (wuz.px_per_sec() == 10);
508 
509  notified = false;
510  otherTriger = false;
511  win.detachChangeNotification();
512  win.nudgeMetric(+1);
513  CHECK (not notified);
514  CHECK (win.px_per_sec() == 400);
515 
516  wuz.nudgeMetric(+1);
517  CHECK (not notified);
518  CHECK (otherTriger);
519  CHECK (win.px_per_sec() == 400);
520  CHECK (wuz.px_per_sec() == 20);
521  }
522 
523 
525  void
527  {
528  ZoomWindow win{0, TimeSpan{_t(0), FSecs(0)}};
529  CHECK (win.visible() == TimeSpan(_t(0), _t(23))); // uses DEFAULT_CANVAS instead of empty TimeSpan
530  CHECK (win.px_per_sec() == 25); // falls back on default initial zoom factor
531  CHECK (win.pxWidth() == 575); // allocates pixels in accordance to default
532 
533  win.setOverallDuration(Duration{FSecs(50)});
534  win.setVisibleDuration(Duration{FSecs(0)});
535  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(50)));
536  CHECK (win.visible() == TimeSpan(_t(0), _t(23))); // falls back to DEFAULT_CANVAS size
537  CHECK (win.pxWidth() == 575); // allocates pixels in accordance to default
538 
539  win.calibrateExtension(0);
540  CHECK (win.px_per_sec() == 25); // stays at default zoom factor
541  CHECK (win.pxWidth() == 1); // retains 1px window size
542  CHECK (win.visible().duration() == _t(1,25)); // visible window has thus 1/25s duration
543  }
544 
545 
547  void
549  {
550  ZoomWindow win{1};
551  win.setVisibleDuration(Duration{FSecs(1,25)});
552  win.setOverallRange(TimeSpan(_t(10), _t(0))); // set an "reversed" overall time range
553  CHECK (win.overallSpan() == TimeSpan(_t(0), _t(10))); // range has been re-oriented forward
554  CHECK (win.visible().duration() == Time(40,0));
555  CHECK (win.px_per_sec() == 25);
556  CHECK (win.pxWidth() == 1);
557 
558  CHECK (TimeSpan(_t(10), _t(0)).duration() == Duration(FSecs(10))); // TimeSpan is always properly oriented by construction
559  }
560 
561 
572  void
574  {
575  Rat poison{_raw(Time::MAX)-101010101010101010, _raw(Time::MAX)+23};
576  CHECK (poison == 206435633551724850_r/307445734561825883);
577  CHECK (2_r/3 < poison and poison < 1); // looks innocuous...
578  CHECK (poison + Time::SCALE < 0); // simple calculations fail due to numeric overflow
579  CHECK (poison * Time::SCALE < 0);
580  CHECK (-6 == rational_cast<gavl_time_t>(poison * Time::SCALE)); // naive conversion to µ-ticks would lead to overflow
581  CHECK (671453 == _raw(Time(FSecs(poison)))); // however the actual conversion routine is safeguarded
582  CHECK (671453.812f == rational_cast<float>(poison)*Time::SCALE);
583 
584  using util::ilog2;
585  CHECK (40 == ilog2 (LIM_HAZARD)); // LIM_HAZARD is based on MAX_INT / Time::Scale
586  CHECK (57 == ilog2 (poison.numerator())); // use the leading bit position as size indicator
587  CHECK (58 == ilog2 (poison.denominator())); // use the maximum of numerator or denominator bit position
588  CHECK (58-40 == 18); // everything beyond LIM_HAZARD counts as "toxic"
589 
590  int toxicity = toxicDegree (poison);
591  CHECK (toxicity == 18);
592  int64_t quant = poison.denominator() >> toxicity; // shift away the excess toxic LSB
593  CHECK (quant == 1172812402961);
594  CHECK (ilog2 (quant) == ilog2 (LIM_HAZARD));
595  Rat detoxed = util::reQuant(poison, quant); // and use this "shortened" denominator for re-quantisation
596  CHECK (detoxed == 787489446837_r/1172812402961); // the resulting fraction uses way smaller numbers
597  CHECK (0.671453834f == rational_cast<float> (poison)); // but yields approximately the same effective value
598  CHECK (0.671453834f == rational_cast<float> (detoxed));
599 
600  CHECK (detoxed+Time::SCALE == 1172813190450446837_r/1172812402961); // result: usual calculations without failure
601  CHECK (Time(FSecs(detoxed)) > Time::ZERO); // can convert re-quantised number to µ-ticks
602  CHECK (671453 == _raw(Time(FSecs(detoxed))));
603  // and resulting µ-ticks will be effectively the same
604  CHECK (1906 == _raw(TimeValue(1280 / rational_cast<long double>(poison))));
605  CHECK (1906 == _raw(TimeValue(1280 / rational_cast<long double>(detoxed))));
606  }
607 
608 
615  void
617  {
618  ZoomWindow win{};
619  CHECK (win.visible() == win.overallSpan()); // by default window spans complete canvas
620  CHECK (win.visible().duration() == _t(23)); // ...and has just some handsome extension
621  CHECK (win.px_per_sec() == 25);
622  CHECK (win.pxWidth() == 575);
623 
624  Rat poison{_raw(Time::MAX)-101010101010101010, _raw(Time::MAX)+23};
625  CHECK (0 < poison and poison < 1);
626 
627  /*--Test-1-----------*/
628  win.setMetric (poison); // inject an evil new value for the metric
629  CHECK (win.visible() == win.overallSpan()); // however, nothing happens
630  CHECK (win.visible().duration() == _t(23)); // since the window is confined to overall canvas size
631  CHECK (win.visible() == TimeSpan(_t(0), _t(23))); // Note: this calculation is fail-safe
632  CHECK (win.px_per_sec() == 25);
633  CHECK (win.pxWidth() == 575);
634 
635  win.setOverallDuration(Duration(Time::MAX)); // second test: expand canvas to allow for actual adjustment
636  CHECK (win.overallSpan().duration() == TimeValue{307445734561825860}); // now canvas has ample size (half the possible maximum size)
637  CHECK (win.overallSpan().duration() == Time::MAX);
638  CHECK (win.visible().duration() == _t(23)); // while the visible part remains unaltered
639 
640  /*--Test-2-----------*/
641  win.setMetric (poison); // Now attempt again to poison the zoom calculations...
642  CHECK (win.overallSpan().duration() == Time::MAX); // overall canvas unchanged
643  CHECK (win.visible().duration() == TimeValue{856350691}); // visible window expanded (a zoom-out, as required)
644  CHECK (win.px_per_sec() == Rat{win.pxWidth()} / _FSecs(win.visible().duration()));
645  float approxPoison = rational_cast<float> (poison); // the provided (poisonous) metric factor...
646  CHECK (approxPoison == 0.671453834f); // ...is approximately the same...
647  float approxNewMetric = rational_cast<float> (win.px_per_sec()); // ...as the actual new metric factor we got
648  CHECK (approxNewMetric == 0.671453893f);
649  CHECK (win.px_per_sec() != poison); // but it is not exactly the same
650  CHECK (win.px_per_sec() < poison); // rather, it is biased towards slightly smaller values
651 
652  Rat poisonousDuration = win.pxWidth() / poison; // Now, to demonstrate this "poison" was actually dangerous
653  CHECK (poisonousDuration == 7071251894921995309_r/8257425342068994); // ...when we attempt to calculate the new duration directly....
654  CHECK (poisonousDuration * Time::SCALE < 0); // ...then a conversion to TimeValue will cause integer wrap
655  CHECK(856.350708f == rational_cast<float> (poisonousDuration)); // yet numerically the duration actually established is almost the same
656  CHECK(856.350708f == rational_cast<float> (_FSecs(win.visible().duration())));
657  CHECK (win.px_per_sec() == 575000000_r/856350691); // the new metric however is comprised of sanitised fractional numbers
658  CHECK (win.pxWidth() == 575); // and the existing pixel width was not changed
659 
660  CHECK (win.overallSpan().start() == Time::ZERO);
661  CHECK (win.overallSpan().duration() == TimeValue{307445734561825860});
662  CHECK (win.visible().duration() == TimeValue{856350691});
663 
664  /*--Test-3-----------*/
665  win.setVisiblePos (poison); // Yet another way to sneak in our toxic value...
666  CHECK (win.overallSpan().start() == Time::ZERO);
667  CHECK (win.overallSpan().duration() == TimeValue{307445734561825860}); // However, all base values turn out unaffected
668  CHECK (win.visible().duration() == TimeValue{856350691});
669 
670  TimeValue targetPos{gavl_time_t(_raw(win.overallSpan().duration()) // based on the overall span...
671  * rational_cast<double> (poison))}; // the given toxic factor would point at that target position
672 
673  CHECK (targetPos == TimeValue{206435633551724864});
674  CHECK (win.visible().start() == TimeValue{206435633106265625}); // the visible window has been moved to enclose this target
675  CHECK (win.visible().end() == TimeValue{206435633962616316});
676  CHECK (win.visible().start() < targetPos);
677  CHECK (win.visible().end() > targetPos);
678 
679  CHECK (win.px_per_sec() == 575000000_r/856350691); // metric and pixel width are retained
680  CHECK (win.pxWidth() == 575);
681 
682 
683  win.setOverallRange(TimeSpan{Time::MAX, Offset{TimeValue(23)}}); // preparation for Test-4 : shift canvas to end of time
684  CHECK (win.overallSpan() == win.visible()); // consequence: window has been capped to canvas size
685  CHECK (win.overallSpan().start() == TimeValue{307445734561825572}); // window now also located at extreme values
686  CHECK (win.overallSpan().end() == TimeValue{307445734561825860});
687  CHECK (win.overallSpan().duration() == TimeValue{288}); // window (and canvas) were expanded to comply to maximum zoom factor
688  CHECK (win.px_per_sec() == 17968750_r/9); // zoom factor was then slightly reduced to match next pixel boundary
689  CHECK (win.pxWidth() == 575); // established pixel size was retained
690 
691  /*--Test-4-----------*/
692  win.setVisiblePos(Time{Time::MIN + TimeValue(13)}); // Test: implicitly provoke poisonous factor through extreme offset
693  CHECK (win.visible().start() == Time::MIN + TimeValue(13)); // even while this position is far off, window start was aligned to it
694  CHECK (win.visible().end() == win.visible().start() + TimeValue{288});
695  CHECK (win.visible().duration() == TimeValue{288});
696 
697  CHECK (win.overallSpan().start() == win.visible().start()); // canvas start at window start
698  CHECK (win.overallSpan().end() == TimeValue{307445734561825860}); // canvas end not changed
699  CHECK (_raw(win.overallSpan().duration()) == 614891469123651707); // canvas size was expanded to encompass changed window position
700  CHECK (win.px_per_sec() == 17968750_r/9); // zoom factor not changed
701  CHECK (win.pxWidth() == 575); // established pixel size retained
702  }
703 
704 
708  void
710  {
711  /*--Test-1-----------*/
712  ZoomWindow win{3, TimeSpan{_t(-1,2), _t(1,2)}}; // setup ZoomWindow to very small pixel size (3px)
713  CHECK (win.overallSpan().duration() == _t(1));
714  CHECK (win.px_per_sec() == 3_r/1);
715  CHECK (win.pxWidth() == 3);
716  win.setOverallRange(TimeSpan{Time::MIN, Time::MAX}); // ...and then also expand canvas to maximal size
717  CHECK (_raw(win.overallSpan().duration()) == 614891469123651720);
718  CHECK (_raw(win.visible().duration()) == 1000000);
719  CHECK (win.px_per_sec() == 3_r/1);
720  CHECK (win.pxWidth() == 3);
721 
722  /*--Test-2-----------*/
723  Rat bruteZoom{3_r/(int64_t{1}<<60)};
724  win.setMetric (bruteZoom); // zoom out beyond what is possible and to a toxic factor
725 
726  CHECK (_raw(win.overallSpan().duration()) == 614891469123651720); // canvas size not changed
727  CHECK (_raw(win.visible().duration()) == 3298534883328000); // window was expanded,
728  CHECK (_raw(win.visible().duration()) < int64_t{1}<<60 ); // ...but not as much as demanded
729  CHECK (_raw(win.visible().duration()) == 3* LIM_HAZARD*1000); // In fact it was capped at a built-in limit based on pixel size,
730  // to prevent formation of dangerous numbers within metric calculations
731  CHECK (win.visible().start() == - win.visible().end()); // window has been expanded symmetrically to existing position
732  CHECK (win.px_per_sec() > bruteZoom); // the actual zoom factor also reflects the applied limitation,
733  CHECK (win.px_per_sec() == 125_r/137438953472); // to ensure the denominator does not exceed LIM_HAZARD
734  CHECK (win.px_per_sec() == 1000_r/LIM_HAZARD);
735  CHECK (win.px_per_sec() == 3 / _FSecs(win.visible().duration())); // and this value also conforms with the pixel size and window duration
736  CHECK (win.pxWidth() == 3);
737 
738  /*--Test-3-----------*/
739  win.setMetric (5_r/std::numeric_limits<int64_t>::max()); // same limiting applies to even more nasty values
740  CHECK (_raw(win.visible().duration()) == 3298534883328000); // still unchanged at limit
741  CHECK (win.px_per_sec() == 125_r/137438953472);
742  CHECK (win.pxWidth() == 3);
743 
744  /*--Test-4-----------*/
745  win.setMetric (1001_r/LIM_HAZARD); // but zooming in more than that limit will be honored
746  CHECK (_raw(win.visible().duration()) == 3295239643684316); // ...window now slightly reduced in size
747  CHECK (_raw(win.visible().duration()) < 3 * LIM_HAZARD*1000);
748  CHECK (win.px_per_sec() > 1000_r/LIM_HAZARD);
749  CHECK (win.px_per_sec() == 1001_r/LIM_HAZARD); // (this is what was requested)
750  CHECK (win.px_per_sec() == 1001_r/1099511627776);
751  CHECK (win.pxWidth() == 3);
752 
753  /*--Test-5-----------*/
754  win.setMetric (1000_r/LIM_HAZARD * 1024_r/1023); // likewise zooming back out slightly below limit is possible
755  CHECK (_raw(win.visible().duration()) == 3295313657856000); // ...window now again slightly increased, but not at maximum size
756  CHECK (_raw(win.visible().duration()) < 3 * LIM_HAZARD*1000);
757  CHECK (win.px_per_sec() > 1000_r/LIM_HAZARD);
758  CHECK (win.px_per_sec() < 1001_r/LIM_HAZARD);
759  CHECK (win.px_per_sec() == 1000_r/LIM_HAZARD * 1024_r/1023); // zoom factor precisely reproduced in this case
760  CHECK (win.px_per_sec() == 125_r/137304735744);
761  CHECK (win.pxWidth() == 3);
762 
763  /*--Test-6-----------*/
764  win.setMetric (1001_r/(LIM_HAZARD-3)); // however, setting »poisonous« factors close below the limit...
765  CHECK (win.px_per_sec() > 1001_r/LIM_HAZARD); // results in a sanitised (simplified) zoom factor
766  CHECK (win.px_per_sec() < 1002_r/LIM_HAZARD);
767  CHECK (1001_r/(LIM_HAZARD-3) == 77_r/84577817521); // This case is especially interesting, since the initial factor isn't »toxic«,
768  // but the resulting duration is not µ-grid aligned, and after fixing that,
769  CHECK (3_r/3295239643675325 * Time::SCALE == 120000_r/131809585747013);// the resulting zoom factor is comprised of very large numbers,
770  CHECK (win.px_per_sec() == 2003_r/2199023255552); // ...which are then simplified and adjusted...
771  CHECK (win.pxWidth() == 3); // ... to match also the pixel size
772 
773  CHECK (_raw(Duration{3_r/(77_r/84577817521)}) == 3295239643675324); // This is the duration we'd expect (truncated down)
774  CHECK (_raw(win.visible().duration()) == 3295239643675325); // ...this is the duration we actually get
775  CHECK (_raw(Duration{3_r/win.px_per_sec()}) == 3293594491590614); // Unfortunately, calculating back from the smoothed zoom-metric
776  // .. would yield a duration way off, with an relative error < 1‰
777  CHECK (2003.0f/2002 - 1 == 0.000499486923f); // The reason for this relative error is the small numerator of 2002
778  // (2002 is increased to 2003 to get above 3px)
779 
780  /*--Test-7-----------*/
781  win.calibrateExtension (1'000'000'000); // implicit drastic zoom-out by increasing the number of pixels
782  CHECK (win.pxWidth() < 1'000'000'000); // however: this number is capped at a fixed maximum
783  CHECK (win.pxWidth() == MAX_PX_WIDTH); // (which „should be enough“ for the time being...)
784  CHECK (win.px_per_sec() == 89407_r/549755813888); // the zoom metric has been adapted, but to a sanitised value
785  CHECK (win.px_per_sec() > Rat{MAX_PX_WIDTH} /MAX_TIMESPAN);
786  CHECK (win.px_per_sec() < Rat{MAX_PX_WIDTH+1}/MAX_TIMESPAN);
787 
788  CHECK (_raw(win.overallSpan().duration()) == 614891469123651720); // overall canvas duration not changed
789  CHECK (_raw(win.visible().duration()) == 614891469123651720); // window duration now expanded to the maximum possible value
790  CHECK (win.overallSpan().end() == TimeValue{ 307445734561825860}); // window now spans the complete time domain
791  CHECK (win.visible().end() == TimeValue{ 307445734561825860});
792  CHECK (win.visible().start() == TimeValue{-307445734561825860});
793 
794  // Note: these parameters build up to really »poisonous« values....
795  CHECK (MAX_PX_WIDTH / _FSecs(win.visible().duration()) == 2500000000_r/15372286728091293);
796  CHECK (MAX_PX_WIDTH * 1000000_r/614891469123651720 == 2500000000_r/15372286728091293);
797  CHECK (win.px_per_sec() * _FSecs(win.visible().duration()) < 0); // we can't even calculate the resulting pxWidth() naively
798  CHECK (rational_cast<float>(win.px_per_sec()) // ...while effectively these values are still correct
799  * rational_cast<float>(_FSecs(win.visible().duration())) == 100000.031f);
800  CHECK (rational_cast<float>(MAX_PX_WIDTH*1000000_r/614891469123651720) == 1.62630329e-07f); // theoretical value
801  CHECK (rational_cast<float>(win.px_per_sec()) == 1.62630386e-07f); // value actually chosen
802  CHECK (win.px_per_sec() == 89407_r/549755813888);
803 
804  /*--Test-8-----------*/
805  win.setMetric (bruteZoom); // And now put one on top by requesting excessive zoom-out!
806  CHECK (_raw(win.overallSpan().duration()) == 614891469123651720); // overall canvas duration not changed
807  CHECK (_raw(win.visible().duration()) == 614891469123651720); // window duration was capped precisely at DURATION_MAX
808  CHECK (win.px_per_sec() == 89407_r/549755813888); // zoom factor and now hitting again the minimum limit
809  CHECK (MAX_PX_WIDTH /(614891469123651720_r/Time::SCALE) == 2500000000_r/15372286728091293); // (this would be the exact factor)
810  CHECK (2500000000_r/15372286728091293 < 89407_r/549755813888); // zoom factor (again) numerically sanitised
811  CHECK (win.pxWidth() == MAX_PX_WIDTH); // pixel count unchanged at maximum
812  }
813 
814 
818  void
820  {
821  /*--Test-1-----------*/
822  ZoomWindow win{559, TimeSpan{Time::MAX, Duration{TimeValue(3)}}}; // setup a very small window clinging to Time::MAX
823  CHECK (win.visible().duration() == TimeValue(280)); // duration expanded due to MAX_ZOOM limit
824  CHECK (win.visible().start() == TimeValue(307445734561825580)); // and properly oriented and aligned within domain
825  CHECK (win.visible().end() == TimeValue(307445734561825860));
826  CHECK (win.visible().end() == Time::MAX);
827  CHECK (win.visible() == win.overallSpan());
828  CHECK (win.px_per_sec() == 559_r/280*Time::SCALE);
829  CHECK (win.px_per_sec() == 13975000_r/7);
830  CHECK (win.pxWidth() == 559);
831 
832  /*--Test-2-----------*/
833  Time anchorPos{15_r/16 * Offset(Time::MIN)};
834  win.setVisiblePos (anchorPos); // scroll to a target position extremely far off
835  CHECK (win.visible().duration() == TimeValue(280)); // window dimensions retained
836  CHECK (win.px_per_sec() == 13975000_r/7);
837  CHECK (win.pxWidth() == 559);
838  CHECK (win.visible().start() > Time::MIN);
839  CHECK (win.visible().start() == anchorPos); // window now at desired position
840  CHECK (win.visible().end() > anchorPos);
841  CHECK (win.visible().start() == TimeValue(-288230376151711744));
842  CHECK (win.visible().end() == TimeValue(-288230376151711464));
843  CHECK (win.overallSpan().start() == win.visible().start()); // canvas expanded accordingly
844  CHECK (win.overallSpan().end() == Time::MAX);
845 
846  /*--Test-3-----------*/
847  win.calibrateExtension (560);
848  CHECK (win.visible().duration() == TimeValue(280)); // effective window dimensions unchanged
849  CHECK (win.px_per_sec() == 2000000_r/1); // but zoom metric slightly adapted
850 
851  win.setOverallDuration (Duration::MAX); // now use maximally expanded canvas
852  Duration targetDur{Duration::MAX - FSecs(23)};
853  win.setVisibleDuration(targetDur); // and demand the duration be expanded almost full size
854 
855  CHECK (win.visible().duration() == targetDur); // actual duration is the value requested
856  CHECK (win.visible().duration() < Duration::MAX);
857  CHECK (win.visible().start() == Time::MIN); // expansion was anchored at previous position
858  CHECK (win.visible().start() < Time::MAX); // and thus the window now clings to the lower end
859  CHECK (win.visible().end() == TimeValue(307445734538825860));
860  CHECK (Time::MAX - win.visible().end() == TimeValue(23*Time::SCALE));
861  CHECK (win.px_per_sec() == 2003_r/2199023255552); // effective zoom metric has been sanitised numerically
862  CHECK (win.pxWidth() == 560); // but pixel count is matched precisely
863 
864  /*--Test-4-----------*/
865  win.setVisiblePos (Rat{std::numeric_limits<int64_t>::max()-23});
866  CHECK (win.visible().duration() == targetDur); // actual duration unchanged
867  CHECK (win.px_per_sec() == 2003_r/2199023255552);
868  CHECK (win.pxWidth() == 560);
869  CHECK (win.visible().end() == Time::MAX); // but window now slinged to the right extreme
870  CHECK (win.visible().start() > Time::MIN);
871  CHECK (win.visible().start() == TimeValue(-307445734538825860));
872 
873  /*--Test-5-----------*/
874  win.calibrateExtension (561); // expand by 1 pixel
875  CHECK (win.visible().duration() > targetDur); // actual duration indeed increased
876  CHECK (win.visible().duration() == Duration::MAX); // and then capped at maximum
877  CHECK (win.visible().end() == Time::MAX); // but while initially the upper bound is increased...
878  CHECK (win.visible().start() == Time::MIN);
879  CHECK (win.px_per_sec() == 2007_r/2199023255552); // the smoothed nominal metric was also increased slightly
880  CHECK (win.pxWidth() == 561);
881 
882  /*--Test-6-----------*/
883  win.setVisibleDuration (Duration::MAX - Duration(TimeValue(1))); // request slightly different window duration
884  CHECK (win.visible().end() == Time::MAX); // by arbitrary choice, the single µ-tick was removed at start
885  CHECK (win.visible().start() == Time::MIN + TimeValue(1));
886  CHECK (win.px_per_sec() == 2007_r/2199023255552); // the smoothed nominal metric was also increased slightly
887  CHECK (win.pxWidth() == 561);
888 
889  win.setVisibleDuration (Duration(TimeValue(1))); // drastically zoom-in
890  CHECK (win.visible().duration() == TimeValue(281)); // ...but we get more than 1 µ-tick
891  CHECK (561_r/_FSecs(TimeValue(1)) > ZOOM_MAX_RESOLUTION); // because the requested window would exceed maximum zoom
892  CHECK (win.px_per_sec() == 561000000_r/281); // and this conflict was resolved by increasing the window
893  CHECK (win.visible().end() == Time::MAX); // while keeping it aligned to the end of the timeline
894  CHECK (win.pxWidth() == 561);
895  }
896 
897 
900  void
902  {
903  ZoomWindow win{ 1, TimeSpan{Time::MAX, Duration{TimeValue(1)}}}; // use window of 1px size zoomed at 1 µ-tick
904  CHECK (win.visible().start() == Time::MAX - TimeValue(1)); // which is aligned to the end of the time domain
905  CHECK (win.visible().duration() == TimeValue(1));
906 
907  win.nudgeVisiblePos (-2); // can be nudged by one window size to the left
908  CHECK (win.visible().start() == Time::MAX - TimeValue(2));
909 
910  win.offsetVisiblePos (Offset{Duration::MAX}); // but excess offset is just absorbed
911  CHECK (win.visible().end() == Time::MAX); // window again positioned at the limit
912  CHECK (win.visible().start() == Time::MAX - TimeValue(1));
913  CHECK (win.visible().duration() == TimeValue(1));
914  CHECK (win.overallSpan().duration() == TimeValue(2));
915  CHECK (win.px_per_sec() == 1000000);
916  CHECK (win.pxWidth() == 1);
917 
918  win.nudgeVisiblePos (std::numeric_limits<int64_t>::min()); // excess nudging likewise absorbed
919  CHECK (win.overallSpan().duration() == Duration::MAX);
920  CHECK (win.visible().duration() == TimeValue(1));
921  CHECK (win.visible().start() == Time::MIN); // window now positioned at lower limit
922  CHECK (win.visible().end() == Time::MIN + TimeValue(1));
923  CHECK (win.px_per_sec() == 1000000);
924  CHECK (win.pxWidth() == 1);
925 
926  win.calibrateExtension (460);
927  win.setVisibleDuration (Duration{Time::MAX - TimeValue(1)}); // arrange window to be 1 µ-tick less than half
928  CHECK (win.visible().duration() == Time::MAX - TimeValue(1));
929  CHECK (win.visible().start() == Time::MIN); // ...so it spans [Time::MIN ... -1]
930  CHECK (win.visible().end() == TimeValue(-1));
931 
932  win.nudgeVisiblePos (+2); // thus nudging two times by half-window size...
933  CHECK (win.visible().end() == Time::MAX - TimeValue(2)); // ...still fits into the time domain
934  CHECK (win.visible().start() == TimeValue(-1));
935  win.nudgeVisiblePos (-1);
936  CHECK (win.visible().start() == TimeValue(-153722867280912930)); // navigation within domain works as expected
937  CHECK (win.visible().end() == TimeValue(+153722867280912929));
938 
939  win.nudgeVisiblePos (+1000); // requesting an excessive nudge...
940  CHECK (ilogb(500.0 * _raw(Time::MAX)) == 67); // which — naively calculated — would overflow 64-bit
941  CHECK (win.visible().start() == TimeValue(+1)); // but the window just stopped aligned to the upper limit
942  CHECK (win.visible().end() == Time::MAX);
943  CHECK (win.pxWidth() == 460);
944  }
945 
946 
949  void
951  { // for setup, request a window crossing time domain bounds
953  CHECK (win.overallSpan().duration() == Duration::MAX); // we get a canvas with the requested extension Duration::MAX
954  CHECK (win.overallSpan().end() == Time::MAX); // but shifted into domain to fit
955  CHECK (win.visible().duration() == LIM_HAZARD * 1000); // the visible window however is limited to be smaller
956  CHECK (win.visible().start()+win.visible().end() == Time::ZERO); // and (since this is a zoom-in) it is centred at origin
957  CHECK (win.px_per_sec() == 1_r/(LIM_HAZARD*1000)*Time::SCALE); // Zoom metric is likewise limited, to keep the numbers manageable
958  CHECK (win.px_per_sec() == 125_r/137438953472);
959  CHECK (win.pxWidth() == 1);
960 
961  win.nudgeVisiblePos (+1); // can work with this tiny window as expected
962  CHECK (win.visible().start() == Time::ZERO);
963  CHECK (win.visible().end() == LIM_HAZARD*1000);
964  CHECK (win.px_per_sec() == 125_r/137438953472);
965  CHECK (win.pxWidth() == 1);
966 
967  win.nudgeMetric (-1); // can not zoom out further
968  CHECK (win.px_per_sec() == 125_r/137438953472);
969  win.nudgeMetric (+1); // but can zoom in
970  CHECK (win.px_per_sec() == 125_r/68719476736);
971  CHECK (win.visible().start() == TimeValue(274877908523000));
972  CHECK (win.visible().end() == TimeValue(824633722411000));
973  CHECK (win.visible().duration() == LIM_HAZARD * 1000 / 2);
974  CHECK (win.pxWidth() == 1);
975 
976  win.setVisiblePos (Time{Time::MAX - TimeValue(23)});
977  CHECK (win.visible().end() == Time::MAX);
978  CHECK (win.visible().duration() == LIM_HAZARD * 1000 / 2);
979  CHECK (win.px_per_sec() == 2_r/(LIM_HAZARD*1000)*Time::SCALE);
980  CHECK (win.pxWidth() == 1);
981 
982  win.setVisibleRange (TimeSpan{Time::MAX - TimeValue(23) // request a window exceeding domain,
983  ,FSecs{LIM_HAZARD, 1001}}); // but with a zoom slightly above minimal-zoom
984  CHECK (win.visible().end() == Time::MAX); // Resulting window is shifted into domain
985  CHECK (win.visible().duration() == Duration(FSecs{LIM_HAZARD, 1001})); // and has the requested extension
986  CHECK (win.visible().duration() == TimeValue(1098413214561438));
987  CHECK ( FSecs(LIM_HAZARD, 1000) > FSecs(LIM_HAZARD, 1001)); // which is indeed smaller than the maximum duration
988  CHECK (win.px_per_sec() == 2003_r/2199023255552);
989  CHECK (win.pxWidth() == 1);
990  }
991 
992 
997  void
999  {
1000  ZoomWindow win{TimeSpan{Time::MIN, Duration{TimeValue(1)}}}; // just request a window spanning the minimally possible value
1001  CHECK (win.overallSpan().duration() == win.visible().duration());
1002  CHECK (win.visible().duration() == TimeValue(1)); // as requested we get a window sized 1 µ-tick
1003  CHECK (win.visible().start() == Time::MIN); // and aligned at the lower domain bound
1004  CHECK (win.visible().end() == Time::MIN + TimeValue(1));
1005  CHECK (win.pxWidth() < ZOOM_MAX_RESOLUTION); // however, can't reach maximum zoom this way
1006  CHECK (win.px_per_sec() == 1000000);
1007  CHECK (win.pxWidth() == 1);
1008 
1009  win.setOverallDuration (Duration{FSecs(1)});
1010  win.calibrateExtension (2); // so... get more pixels to work with
1011  CHECK (win.visible().duration() == TimeValue(2)); // ... they are used to expand the window
1012  CHECK (win.px_per_sec() == 1000000); // .. resting at exiting zoom level
1013 
1014  win.setMetric (ZOOM_MAX_RESOLUTION);
1015  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION); // now able to reach the maximum zoom level
1016  CHECK (win.px_per_sec() == 2000000); // (which is more or less an arbitrary choice)
1017  CHECK (win.visible().start() == Time::MIN);
1018  CHECK (win.visible().end() == Time::MIN + TimeValue(1)); // while the actual window size is µ-grid aligned
1019  CHECK (win.pxWidth() == 2); // meaning we can not zoom in without limit
1020 
1021  win.nudgeVisiblePos (+1); // scroll one »step« to the right
1022  CHECK (win.visible().start() == Time::MIN + TimeValue(1)); // yet this step has been increased to a full window size,
1023  CHECK (win.visible().end() == Time::MIN + TimeValue(2)); // since a smaller scoll-step can not be represented in µ-ticks
1024  CHECK (win.visible().duration() == TimeValue(1));
1025  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION);
1026 
1027  win.calibrateExtension (3); // add a third pixel
1028  CHECK (win.visible().duration() == TimeValue(2)); // window extension increased to the next full µ-tick
1029  CHECK (win.px_per_sec() == 3_r/4 * ZOOM_MAX_RESOLUTION); // and the rest was absorbed into the zoom scale
1030  CHECK (win.visible().start() == Time::MIN + TimeValue(1));
1031  CHECK (win.visible().end() == Time::MIN + TimeValue(3));
1032  CHECK (win.pxWidth() == 3);
1033 
1034  win.setVisibleDuration (Duration{TimeValue(17)});
1035  CHECK (win.px_per_sec() == 3000000_r/17);
1036  win.setVisibleDuration (Duration{TimeValue(16)});
1037  CHECK (win.px_per_sec() == 187500);
1038  win.setVisibleDuration (Duration{TimeValue(15)});
1039  CHECK (win.px_per_sec() == 200000);
1040  CHECK (win.visible().start() == Time::MIN + TimeValue(1));
1041  CHECK (win.visible().end() == Time::MIN + TimeValue(16));
1042 
1043  win.nudgeMetric (-1);
1044  CHECK (win.px_per_sec() == 100000);
1045  CHECK (win.visible().duration() == TimeValue(30));
1046  win.nudgeMetric (+2);
1047  CHECK (win.px_per_sec() == 375000);
1048  CHECK (win.visible().duration() == TimeValue(8));
1049  win.nudgeMetric (+1);
1050  CHECK (win.px_per_sec() == 750000);
1051  CHECK (win.visible().duration() == TimeValue(4));
1052 
1053  win.setMetric (2_r/3 * ZOOM_MAX_RESOLUTION);
1054  CHECK (win.px_per_sec() == 1_r/2 * ZOOM_MAX_RESOLUTION); // can't do that, Dave
1055  CHECK (win.px_per_sec() == 1000000);
1056  CHECK (win.visible().duration() == TimeValue(3));
1057  CHECK (win.visible().start() == Time::MIN + TimeValue(1));
1058  CHECK (win.visible().end() == Time::MIN + TimeValue(4));
1059 
1060  win.nudgeVisiblePos (-5);
1061  CHECK (win.visible().start() == Time::MIN + TimeValue(0)); // stopped at lower time domain limit
1062  CHECK (win.visible().end() == Time::MIN + TimeValue(3));
1063  CHECK (win.visible().duration() == TimeValue(3));
1064 
1065  win.calibrateExtension (MAX_PX_WIDTH); // similar logic applies when using much more pixels
1066  CHECK (win.pxWidth() == 100000);
1067  CHECK (win.visible().duration() == TimeValue(100000));
1068  CHECK (win.px_per_sec() == 1_r/2 * ZOOM_MAX_RESOLUTION);
1069  CHECK (win.visible().start() == Time::MIN + TimeValue(0));
1070  CHECK (win.visible().end() == Time::MIN + TimeValue(100000));
1071 
1072  win.setMetric (3_r/2 * ZOOM_MAX_RESOLUTION);
1073  CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION); // that's all we get
1074  CHECK (win.visible().duration() == TimeValue(50000)); // (until someone comes up with a good use case for showing more)
1075  CHECK (win.visible().end() == Time::MIN + TimeValue(50000));
1076  CHECK (win.pxWidth() == 100000);
1077  }
1078  };
1079 
1080 
1082  LAUNCHER (ZoomWindow_test, "unit gui");
1083 
1084 
1085 }}} // namespace stage::model::test
static const Duration MAX
maximum possible temporal extension
Definition: timevalue.hpp:507
void setVisibleDuration(Duration duration)
explicitly set the duration of the visible window range, working around the relative anchor point; po...
void setRanges(TimeSpan overall, TimeSpan visible)
Set both the overall canvas, as well as the visible part within that canvas.
const int64_t LIM_HAZARD
Maximum quantiser to be handled in fractional arithmetics without hazard.
Definition: run.hpp:40
static const gavl_time_t SCALE
Number of micro ticks (µs) per second as basic time scale.
Definition: timevalue.hpp:167
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:299
A component to ensure uniform handling of zoom scale and visible interval on the timeline.
Simplistic test class runner.
void nudgeVisiblePos(int64_t steps)
scroll by increments of half window size, possibly expanding.
Lumiera GTK UI implementation root.
Definition: guifacade.cpp:37
void nudgeMetric(int steps)
scale up or down on a 2-logarithmic scale.
A collection of frequently used helper functions to support unit testing.
const Rat ZOOM_MAX_RESOLUTION
the deepest zoom is to use 2px per micro-tick
void setOverallRange(TimeSpan range)
redefine the overall canvas range.
Offset measures a distance in time.
Definition: timevalue.hpp:358
Duration is the internal Lumiera time metric.
Definition: timevalue.hpp:468
Abstraction: the current zoom- and navigation state of a view, possibly in multiple dimensions...
A time interval anchored at a specific point in time.
Definition: timevalue.hpp:573
void calibrateExtension(uint pxWidth)
Define the extension of the window in pixels.
basic constant internal time value.
Definition: timevalue.hpp:133
static const Time MAX
Definition: timevalue.hpp:309