Timeline: finish ZoomWindow implementation and boundrary tests

This commit is contained in:
Fischlurch 2022-12-18 03:47:40 +01:00
parent e436023ef9
commit 52d3231226
6 changed files with 220 additions and 35 deletions

View file

@ -64,17 +64,12 @@ typedef unsigned int uint;
#include "lib/format-cout.hpp"
#include "lib/test/test-helper.hpp"
#include "lib/test/diagnostic-output.hpp"
#include "lib/util.hpp"
#include <functional>
#include <string>
using std::string;
#define SHOW_TYPE(_TY_) \
cout << "typeof( " << STRINGIFY(_TY_) << " )= " << lib::meta::typeStr<_TY_>() <<endl;
#define SHOW_EXPR(_XX_) \
cout << "Probe " << STRINGIFY(_XX_) << " ? = " << _XX_ <<endl;
template<class TAR>

View file

@ -0,0 +1,68 @@
/*
DIAGNOSTIC-OUTPUT.hpp - some helpers for investigation and value output
Copyright (C) Lumiera.org
2022, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file diagnostic-output.hpp
** Helpers typically used while writing tests.
** In the finished test, we rather want values be asserted explicitly in the
** test code, but while building the test, it is often necessary to see the types
** and the actual values of intermediary results, to get a clue where matters go south.
**
** @see test-helper.hpp
**
*/
#ifndef LIB_TEST_DIAGNOSTIC_OUTPUT_H
#define LIB_TEST_DIAGNOSTIC_OUTPUT_H
#include "lib/format-cout.hpp"
#include "lib/test/test-helper.hpp"
#include <boost/lexical_cast.hpp>
#include <string>
using std::string;
namespace lib {
namespace test{
}} // namespace lib::test
/* === test helper macros === */
/**
* Macro to print types and expressions to STDOUT,
* using Lumiera's string conversion framework
*/
#define SHOW_TYPE(_TY_) \
cout << "typeof( " << STRINGIFY(_TY_) << " )= " << lib::test::showType<_TY_>() <<endl;
#define SHOW_EXPR(_XX_) \
cout << "#--◆--# " << STRINGIFY(_XX_) << " ? = " << _XX_ <<endl;
#endif /*LIB_TEST_DIAGNOSTIC_OUTPUT_H*/

View file

@ -93,6 +93,8 @@ namespace time {
const Offset Offset::ZERO (Time::ZERO);
const FSecs FSEC_MAX{std::numeric_limits<int64_t>::max() / lib::time::TimeValue::SCALE};
Literal DIAGNOSTIC_FORMAT{"%s%01d:%02d:%02d.%03d"};
@ -339,9 +341,16 @@ lumiera_tmpbuf_print_time (gavl_time_t time)
return buffer;
}
/// @todo this utility function could be factored out into a `FSecs` or `RSec` class ///////////////////////TICKET #1262
gavl_time_t
lumiera_rational_to_time (FSecs const& fractionalSeconds)
{
// avoid numeric wrap from values not representable as 64bit µ-ticks
if (abs(fractionalSeconds) > lib::time::FSEC_MAX)
return (fractionalSeconds < 0? -1:+1)
* std::numeric_limits<int64_t>::max();
return gavl_time_t(util::reQuant (fractionalSeconds.numerator()
,fractionalSeconds.denominator()
,lib::time::TimeValue::SCALE

View file

@ -50,6 +50,9 @@
** - the overall TimeSpan of the timeline, defining a start and end time
** - the visible interval (window), likewise modelled as time::TimeSpan
** - the scale defined as pixels per second
** @todo as of 12/2022 it rather seems the more general navigation should be abstracted
** at a higher level, leaving ZoomWindow mostly focused on time scale handling.
**
**
** # Interactions
**
@ -199,7 +202,10 @@ namespace model {
}
/**
/******************************************************//**
* A component to ensure uniform handling of zoom scale
* and visible interval on the timeline. Changes through
* the mutator functions are validated and harmonised to
@ -237,7 +243,7 @@ namespace model {
}
ZoomWindow (TimeSpan timeline =TimeSpan{Time::ZERO, DEFAULT_CANVAS})
: ZoomWindow{0, timeline} //see ensureConsistent()
: ZoomWindow{0, timeline} //see establishMetric()
{ }
TimeSpan
@ -499,7 +505,7 @@ namespace model {
* is thus specific to the ZoomWindow implementation. To sanitise, the denominator
* is reduced logarithmically (bit-shift) sufficiently and then used as new quantiser,
* thus ensuring that both denominator (=quantiser) and numerator are below limit.
* @warning the rational number must not be to large overall; this heuristic will fail
* @warning the rational number must not be too large overall; this heuristic will fail
* on fractions with very large numerator and small denominator however, for
* the ZoomWindow, this case is not relevant, since the zoom factor is limited,
* and other usages of rational numbers can be range checked explicitly.

View file

@ -25,18 +25,51 @@
** The timeline uses the abstraction of an »Zoom Window«
** to define the scrolling and temporal scaling behaviour uniformly.
** This unit test verifies this abstracted behaviour against the spec.
**
** # Fractional Seconds
**
** A defining trait of the ZoomWindow implementation as it stands 12/2022 is the use
** of integer fractions for most scale and time interval calculations. The typical media
** handling operations often rely on denomination into a divisor defined scale be it
** seconds divided by frame count (25fps), or be it audio samples like 1/96000 sec.
** And for presentation in the UI, these uneven fractions need to be broken down into
** a fixed pixel count, while the zoom factor can vary over several orders of magnitude.
** Integer fractions are a technically brilliant solution to cope with this challenge,
** without rounding discrepancies and accumulation of errors.
**
** However, there is a catch: The way fractional arithmetics are handled leads to lots
** of multiplications, with the tendency to build up very large irreducible numbers, both in
** numerator and denominator. In worst case, numeric wrap-around can happen even at seemingly
** innocuous places. In an attempt to maintain the benefits of integer fraction arithmetics,
** for ZoomWindow a set of »coping strategies« was developed, to detect and control the cases
** when numbers go south. This approach is based on the observation that almost all
** everyday time calculations happen within a rather limited domain, while the extended
** time domain of years and centuries rather serves as a theoretical headroom. Thus it
** seems reasonable to benefit from integer fractions within this everyday range, under
** the condition that computations can be kept from derailing totally, when entering
** the extended domain.
**
** To this end, we use the trick of introducing a minute numeric error, by re-quantising
** huge numbers into a scale with a smaller denominator. We introduce the notion of »toxic«
** numbers, which are defined by figures above 2^40 irrespective if in numerator or in
** denominator. This rather arbitrary choice is based on the observation that most
** computation paths require to multiply with Time::SCALE (the µ-tick scale of 10^6),
** which together with 2^40 just fits into the value range of int64_t. Thus, into all
** crucial computation paths, a function `detox()` is wired, which remains inactive for
** regular values, but automatically _sanitises extreme values._ Together with the
** safety headroom built into the limits of the Lumiera lib::time::Time domain,
** this allows to handle all valid time points and represent even the largest
** possible lib::time::Duration::MAX.
**
** A major part of this test is dedicated to covering those hypothetical corner cases
** and to ensure the defined behaviour can be maintained even under extreme conditions.
**
*/
#include "lib/test/run.hpp"
#include "lib/test/test-helper.hpp"
#include "stage/model/zoom-window.hpp"
#include "lib/format-cout.hpp"//////////////TODO
#define SHOW_TYPE(_TY_) \
cout << "typeof( " << STRINGIFY(_TY_) << " )= " << lib::meta::typeStr<_TY_>() <<endl;
#define SHOW_EXPR(_XX_) \
cout << "#--◆--# " << STRINGIFY(_XX_) << " ? = " << _XX_ <<endl;
//////////////////////////////////////////////////////////////////////////////TODO
namespace stage{
@ -61,6 +94,7 @@ namespace test {
* - setting a visible position
* - nudging the position
* - nudging the scale factor
* @remark the `safeguard_*` tests focus on the boundary cases.
* @see zoom-window.hpp
*/
class ZoomWindow_test : public Test
@ -121,7 +155,7 @@ namespace test {
/** @test verify the possible variations for initial setup of the zoom window
* - can be defined either the canvas duration,
* - can be defined either with the canvas duration,
* or an explicit extension given in pixels, or both
* - after construction, visible window always covers whole canvas
* - window extension, when given, defines the initial metric
@ -156,7 +190,7 @@ namespace test {
}
/** @test verify defining and retaining of the effective extension in pixels
/** @test verify defining and retaining the effective extension in pixels
* - changes to the extension are applied by adjusting the visible window
* - visible window's start position is maintained
* - unless the resulting window would exceed the overall canvas,
@ -525,7 +559,7 @@ namespace test {
ZoomWindow win{1};
win.setVisibleDuration(Duration{FSecs(1,25)});
win.setOverallRange(TimeSpan(_t(10), _t(0))); // set an "reversed" overall time range
CHECK (win.overallSpan() == TimeSpan(_t(0), _t(10))); // range has been oriented forward
CHECK (win.overallSpan() == TimeSpan(_t(0), _t(10))); // range has been re-oriented forward
CHECK (win.visible().duration() == Time(40,0));
CHECK (win.px_per_sec() == 25);
CHECK (win.pxWidth() == 1);
@ -552,7 +586,7 @@ namespace test {
CHECK (2_r/3 < poison and poison < 1); // looks innocuous...
CHECK (poison + Time::SCALE < 0); // simple calculations fail due to numeric overflow
CHECK (poison * Time::SCALE < 0);
CHECK (-6 == rational_cast<gavl_time_t>(poison * Time::SCALE)); // naive conversion to µ-ticks would leads to overflow
CHECK (-6 == rational_cast<gavl_time_t>(poison * Time::SCALE)); // naive conversion to µ-ticks would lead to overflow
CHECK (671453 == _raw(Time(FSecs(poison)))); // however the actual conversion routine is safeguarded
CHECK (671453.812f == rational_cast<float>(poison)*Time::SCALE);
@ -598,6 +632,7 @@ namespace test {
Rat poison{_raw(Time::MAX)-101010101010101010, _raw(Time::MAX)+23};
CHECK (0 < poison and poison < 1);
/*--Test-1-----------*/
win.setMetric (poison); // inject an evil new value for the metric
CHECK (win.visible() == win.overallSpan()); // however, nothing happens
@ -922,14 +957,45 @@ namespace test {
*/
void
safeguard_verySmall()
{
// SHOW_EXPR(win.overallSpan());
// SHOW_EXPR(_raw(win.overallSpan().duration()));
// SHOW_EXPR(_raw(win.visible().duration()));
// SHOW_EXPR(_raw(win.visible().start()));
// SHOW_EXPR(_raw(win.visible().end()));
// SHOW_EXPR(win.px_per_sec());
// SHOW_EXPR(win.pxWidth());
{ // for setup, request a window crossing time domain bounds
ZoomWindow win{ 1, TimeSpan{Time::MAX - TimeValue(23), Duration::MAX}};
CHECK (win.overallSpan().duration() == Duration::MAX); // we get a canvas with the requested extension Duration::MAX
CHECK (win.overallSpan().end() == Time::MAX); // but shifted into domain to fit
CHECK (win.visible().duration() == LIM_HAZARD * 1000); // the visible window however is limited to be smaller
CHECK (win.visible().start()+win.visible().end() == Time::ZERO); // and (since this is a zoom-in) it is centred at origin
CHECK (win.px_per_sec() == 1_r/(LIM_HAZARD*1000)*Time::SCALE); // Zoom metric is likewise limited, to keep the numbers manageable
CHECK (win.px_per_sec() == 125_r/137438953472);
CHECK (win.pxWidth() == 1);
win.nudgeVisiblePos (+1); // can work with this tiny window as expected
CHECK (win.visible().start() == Time::ZERO);
CHECK (win.visible().end() == LIM_HAZARD*1000);
CHECK (win.px_per_sec() == 125_r/137438953472);
CHECK (win.pxWidth() == 1);
win.nudgeMetric (-1); // can not zoom out further
CHECK (win.px_per_sec() == 125_r/137438953472);
win.nudgeMetric (+1); // but can zoom in
CHECK (win.px_per_sec() == 125_r/68719476736);
CHECK (win.visible().start() == TimeValue(274877908523000));
CHECK (win.visible().end() == TimeValue(824633722411000));
CHECK (win.visible().duration() == LIM_HAZARD * 1000 / 2);
CHECK (win.pxWidth() == 1);
win.setVisiblePos (Time{Time::MAX - TimeValue(23)});
CHECK (win.visible().end() == Time::MAX);
CHECK (win.visible().duration() == LIM_HAZARD * 1000 / 2);
CHECK (win.px_per_sec() == 2_r/(LIM_HAZARD*1000)*Time::SCALE);
CHECK (win.pxWidth() == 1);
win.setVisibleRange (TimeSpan{Time::MAX - TimeValue(23) // request a window exceeding domain,
,FSecs{LIM_HAZARD, 1001}}); // but with a zoom slightly above minimal-zoom
CHECK (win.visible().end() == Time::MAX); // Resulting window is shifted into domain
CHECK (win.visible().duration() == Duration(FSecs{LIM_HAZARD, 1001})); // and has the requested extension
CHECK (win.visible().duration() == TimeValue(1098413214561438));
CHECK ( FSecs(LIM_HAZARD, 1000) > FSecs(LIM_HAZARD, 1001)); // which is indeed smaller than the maximum duration
CHECK (win.px_per_sec() == 2003_r/2199023255552);
CHECK (win.pxWidth() == 1);
}

View file

@ -38219,6 +38219,7 @@
</node>
</node>
<node CREATED="1666965004641" ID="ID_748757781" MODIFIED="1666965813264" TEXT="Interaction Procedure">
<linktarget COLOR="#955977" DESTINATION="ID_748757781" ENDARROW="Default" ENDINCLINATION="-318;688;" ID="Arrow_ID_1882192055" SOURCE="ID_867107887" STARTARROW="None" STARTINCLINATION="-49;-276;"/>
<node CREATED="1666965028506" ID="ID_1718237033" MODIFIED="1666965700555" TEXT="mutators">
<icon BUILTIN="full-1"/>
<node CREATED="1666965070532" ID="ID_466715389" MODIFIED="1666965073074" TEXT="set metric"/>
@ -38469,9 +38470,34 @@
</body>
</html></richcontent>
</node>
<node CREATED="1671324422426" ID="ID_1192793970" MODIFIED="1671324572988">
<richcontent TYPE="NODE"><html>
<head>
</head>
<body>
<p>
Zoom-Metrik verwendet <b>integer-Br&#252;che</b>
</p>
</body>
</html></richcontent>
<icon BUILTIN="forward"/>
<node COLOR="#435e98" CREATED="1671324446051" ID="ID_1112836860" MODIFIED="1671324565631" TEXT="kann damit auch unebene Teilungen der Zeitskala sauber handhaben">
<icon BUILTIN="idea"/>
</node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1666966669487" ID="ID_1477573565" MODIFIED="1667093086439" TEXT="testgetrieben entwickelt">
<icon BUILTIN="pencil"/>
<node COLOR="#435e98" CREATED="1671324465573" ID="ID_186471302" MODIFIED="1671324563688" TEXT="erfordert Sicherheitsma&#xdf;nahmen f&#xfc;r sehr gro&#xdf;e Zahlen">
<icon BUILTIN="messagebox_warning"/>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1671324522229" ID="ID_1590349109" MODIFIED="1671324559550" TEXT="Gefahr von numeric-wrap">
<icon BUILTIN="clanbomber"/>
</node>
<node COLOR="#338800" CREATED="1671324535163" ID="ID_298282383" MODIFIED="1671324555746" TEXT="L&#xf6;sung: re-Quantisierung (minimalen Fehler einf&#xfc;hren)">
<icon BUILTIN="button_ok"/>
</node>
</node>
</node>
</node>
<node COLOR="#338800" CREATED="1666966669487" FOLDED="true" ID="ID_1477573565" MODIFIED="1671324271010" TEXT="testgetrieben entwickelt">
<icon BUILTIN="button_ok"/>
<node COLOR="#338800" CREATED="1667093097050" ID="ID_1662709264" MODIFIED="1667093103756" TEXT="verify_simpleUsage">
<icon BUILTIN="button_ok"/>
<node CREATED="1667093106907" ID="ID_1255657275" MODIFIED="1667093118644" TEXT="nudge Zoom-Faktor"/>
@ -39562,8 +39588,8 @@
<icon BUILTIN="button_ok"/>
</node>
</node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1668180004015" ID="ID_1355163433" MODIFIED="1671235929237" TEXT="extreme Grenzf&#xe4;lle abtesten">
<icon BUILTIN="pencil"/>
<node COLOR="#338800" CREATED="1668180004015" ID="ID_1355163433" MODIFIED="1671324161416" TEXT="extreme Grenzf&#xe4;lle abtesten">
<icon BUILTIN="button_ok"/>
<node COLOR="#338800" CREATED="1668212345981" ID="ID_1473341053" MODIFIED="1668212365788" TEXT="leer konstruiert &#x27f9; default-canvas">
<icon BUILTIN="button_ok"/>
</node>
@ -41466,8 +41492,8 @@
</node>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668180054302" ID="ID_1604190635" MODIFIED="1668180136078" TEXT="1 Pixel">
<icon BUILTIN="flag-yellow"/>
<node COLOR="#338800" CREATED="1668180054302" ID="ID_1604190635" MODIFIED="1671324160042" TEXT="1 Pixel">
<icon BUILTIN="button_ok"/>
<node CREATED="1670719662361" ID="ID_1680322812" MODIFIED="1670719685207" TEXT="maximal gef&#xe4;hrlich f&#xfc;r die Metrik-Berechnung">
<node COLOR="#990000" CREATED="1670719706106" ID="ID_266339038" MODIFIED="1670889773325" TEXT="Newton-Optimierung m&#xf6;glicherweise &#x27d8;">
<arrowlink COLOR="#7d5c62" DESTINATION="ID_1040949384" ENDARROW="Default" ENDINCLINATION="-541;29;" ID="Arrow_ID_1071041256" STARTARROW="None" STARTINCLINATION="1170;80;"/>
@ -41477,16 +41503,27 @@
<icon BUILTIN="idea"/>
</node>
</node>
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1671294771824" ID="ID_115927706" MODIFIED="1671294789194" TEXT="numeric-wrap entdeckt">
<node COLOR="#435e98" CREATED="1671294771824" ID="ID_115927706" MODIFIED="1671322048434" TEXT="numeric-wrap entdeckt">
<icon BUILTIN="broken-line"/>
<node CREATED="1671295216805" ID="ID_987376979" MODIFIED="1671295239222" TEXT="In der conversion FSecs &#x27fc; &#xb5;-tick"/>
<node CREATED="1671295240330" ID="ID_1742063347" MODIFIED="1671295249934" TEXT="bei &#xfc;bergro&#xdf;en Werten">
<icon BUILTIN="info"/>
</node>
<node COLOR="#338800" CREATED="1671322050278" ID="ID_1702542630" MODIFIED="1671322066067" TEXT="Limit in den Konverter">
<icon BUILTIN="button_ok"/>
</node>
</node>
</node>
<node CREATED="1670719689323" ID="ID_794436463" MODIFIED="1670719702317" TEXT="mit m&#xf6;glichst gro&#xdf;en Zeiten kombinieren"/>
<node CREATED="1671294759109" ID="ID_531574116" MODIFIED="1671294770708" TEXT="&#xe4;hnliche Operationen wie bereits f&#xfc;r die giftigen Faktoren"/>
<node COLOR="#338800" CREATED="1671294759109" ID="ID_531574116" MODIFIED="1671324166659" TEXT="&#xe4;hnliche Operationen wie bereits f&#xfc;r die giftigen Faktoren">
<icon BUILTIN="button_ok"/>
</node>
<node COLOR="#338800" CREATED="1671324210414" ID="ID_1973605620" MODIFIED="1671324233682" TEXT="sehr gro&#xdf; skalieren und Limit LIM_HAZARD*1000">
<icon BUILTIN="button_ok"/>
</node>
<node COLOR="#338800" CREATED="1671324243418" ID="ID_596210372" MODIFIED="1671324255033" TEXT="Scroll / Zoom-Nudge">
<icon BUILTIN="button_ok"/>
</node>
</node>
<node COLOR="#338800" CREATED="1668264626100" ID="ID_45091906" MODIFIED="1671235896011" TEXT="giftige Br&#xfc;che">
<icon BUILTIN="button_ok"/>
@ -42830,6 +42867,10 @@
</node>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1671324283797" ID="ID_867107887" MODIFIED="1671324383048" TEXT="gem&#xe4;&#xdf; Requirement-Konzept integrieren">
<arrowlink COLOR="#955977" DESTINATION="ID_748757781" ENDARROW="Default" ENDINCLINATION="-318;688;" ID="Arrow_ID_1882192055" STARTARROW="None" STARTINCLINATION="-49;-276;"/>
<icon BUILTIN="flag-yellow"/>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1667488193842" ID="ID_1347640673" MODIFIED="1667488208549" TEXT="Invarianten">
<font BOLD="true" NAME="SansSerif" SIZE="14"/>