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