From f2ef893adb3ef363cd3d4a1863dd384eda059ba3 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 4 Nov 2022 03:40:36 +0100 Subject: [PATCH] Timeline: complete specification of ZoomWindow expected behaviour Writing this specification unveiled a limitation of our internal time base implementation, which is a 64bit microsecond grid. As it turns out, any grid based time representation will always be not precise enough to handle some relevant time specifications, which are defined by a divisor. Most notably this affects the precise display of frame duration in the GUI, and even more relevant, the sample accurate editing of sound in the timeline. Thus I decided to perform the internal computation in ZoomWindow as rational numbers, based on boost::rational Note: implementation stubbed only, test fails --- src/lib/rational.hpp | 80 ++++++ src/stage/model/zoom-metric.hpp | 4 +- src/stage/model/zoom-window.hpp | 31 ++- tests/stage/model/zoom-window-test.cpp | 335 ++++++++++++++++++++++-- wiki/renderengine.html | 18 +- wiki/thinkPad.ichthyo.mm | 347 ++++++++++++++++++++++++- 6 files changed, 782 insertions(+), 33 deletions(-) create mode 100644 src/lib/rational.hpp diff --git a/src/lib/rational.hpp b/src/lib/rational.hpp new file mode 100644 index 000000000..6a7eef09b --- /dev/null +++ b/src/lib/rational.hpp @@ -0,0 +1,80 @@ +/* + RATIONAL.hpp - support for precise rational arithmetics + + Copyright (C) Lumiera.org + 2022, Hermann Vosseler + + 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 rational.hpp + ** Rational number support, based on `boost::rational`. + ** As an extension to integral arithmetics, rational numbers can be defined + ** as a pair (numerator, denominator); since most calculations imply multiplication + ** by common factors, each calculation will be followed by normalisation to greatest + ** common denominator, to keep numbers within value range. Obviously, this incurs + ** a significant performance penalty — while on the other hand allowing for lossless + ** computations on fractional scales, which can be notoriously difficult to handle + ** with floating point numbers. The primary motivation for using this number format + ** is for handling fractional time values properly, e.g 1/30 sec or 1/44100 sec. + ** + ** The underlying implementation from boost::rational can be parametrised with various + ** integral data types; since our time handling is based on 64bit integers, we mainly + ** use the specialisation `boost::rational`. + ** + ** @note all compatible integral types can be automatically converted to rational + ** numbers, which is a lossless conversion. The opposite is not true: to get + ** a "ordinary" number — be it integral or floating point — an explicit + ** conversion using `rational_cast (fraction)` is necessary, which + ** performs the division of `numerator/denominator` in the target value domain. + ** + ** @see zoom-window.hpp + ** @see timevalue.hpp + */ + + +#ifndef LIB_RATIONAL_H +#define LIB_RATIONAL_H + + +#include +#include + + + +namespace util { + + using Rat = boost::rational; + using boost::rational_cast; + +} // namespace util + + +/** + * user defined literal for constant rational numbers. + * \code + * Rat twoThirds = 2_r/3; + * \endcode + */ +inline util::Rat +operator""_r (unsigned long long num) +{ + return util::Rat{num}; +} + + +#endif /*LIB_RATIONAL_H*/ diff --git a/src/stage/model/zoom-metric.hpp b/src/stage/model/zoom-metric.hpp index 07a14888e..33f549aa6 100644 --- a/src/stage/model/zoom-metric.hpp +++ b/src/stage/model/zoom-metric.hpp @@ -93,13 +93,13 @@ namespace model { int translateTimeToPixels (TimeValue startTimePoint) const override { - return _raw(startTimePoint) * zoomWindow_.px_per_sec() / Time::SCALE; + return rational_cast (_raw(startTimePoint) * zoomWindow_.px_per_sec() / Time::SCALE); ////////////////////TICKET #1196 : support canvas origin offset } TimeValue applyScreenDelta(Time anchor, double deltaPx) const override { - return anchor + TimeValue{gavl_time_t(Time::SCALE * deltaPx / zoomWindow_.px_per_sec())}; + return anchor + TimeValue{rational_cast (int64_t(Time::SCALE * deltaPx) / zoomWindow_.px_per_sec())}; //////TODO correct yet confusingly written } }; diff --git a/src/stage/model/zoom-window.hpp b/src/stage/model/zoom-window.hpp index 5d815a42a..88619b497 100644 --- a/src/stage/model/zoom-window.hpp +++ b/src/stage/model/zoom-window.hpp @@ -79,6 +79,7 @@ #include "lib/error.hpp" +#include "lib/rational.hpp" #include "lib/time/timevalue.hpp" #include "lib/nocopy.hpp" //#include "lib/idi/entry-id.hpp" @@ -101,6 +102,9 @@ namespace model { using lib::time::FSecs; using lib::time::Time; + using util::Rat; + using util::rational_cast; + namespace { /** the deepest zoom is to use 2px per micro-tick */ const uint ZOOM_MAX_RESOLUTION = 2 * TimeValue::SCALE; @@ -125,7 +129,7 @@ namespace model { { TimeVar startAll_, afterAll_, startWin_, afterWin_; - uint px_per_sec_; + uint px_per_sec_; ///////////////////TODO use rational public: ZoomWindow (TimeSpan timeline =TimeSpan{Time::ZERO, FSecs(23)}) @@ -136,6 +140,14 @@ namespace model { , px_per_sec_{25} { } + ZoomWindow (uint pxWidth, TimeSpan timeline =TimeSpan{Time::ZERO, FSecs(23)}) + : startAll_{timeline.start()} + , afterAll_{nonEmpty(timeline.end())} + , startWin_{startAll_} + , afterWin_{afterAll_} + , px_per_sec_{25} + { } + TimeSpan overallSpan() const { @@ -148,17 +160,30 @@ namespace model { return TimeSpan{startWin_, afterWin_}; } - uint + Rat px_per_sec() const { return px_per_sec_; } + uint + pxWidth() const + { + REQUIRE (0 < _raw(afterWin_ - startWin_)); + return rational_cast (px_per_sec() / FSecs(afterWin_-startWin_)); + } + /* === Mutators === */ void - setMetric (uint px_per_sec) + calibrateExtension (uint pxWidth) + { + UNIMPLEMENTED ("calibrateExtension"); + } + + void + setMetric (Rat px_per_sec) { UNIMPLEMENTED ("setMetric"); } diff --git a/tests/stage/model/zoom-window-test.cpp b/tests/stage/model/zoom-window-test.cpp index d18d0fe67..5446ed137 100644 --- a/tests/stage/model/zoom-window-test.cpp +++ b/tests/stage/model/zoom-window-test.cpp @@ -28,15 +28,7 @@ #include "lib/test/run.hpp" #include "lib/test/test-helper.hpp" #include "stage/model/zoom-window.hpp" -//#include "lib/util.hpp" -//#include -//#include - - -//using util::isSameObject; -//using std::make_unique; -//using std::move; namespace stage{ @@ -44,14 +36,10 @@ namespace model{ namespace test { - namespace { // Test fixture... + namespace { // simplified notation for expected results... -// template -// struct DummyWidget -// : public sigc::trackable -// { -// X val = 1 + rand() % 100; -// }; + inline Time _t(int secs) { return Time(FSecs(secs)); } + inline Time _t(int s, int div) { return Time(FSecs(s,div)); } } @@ -73,7 +61,21 @@ namespace test { virtual void run (Arg) { + // Explanation of the notation used in this test... + CHECK (_t(10) == Time{FSecs(10)}); // Time point at t = 10sec + CHECK (_t(10,3) == Time{FSecs(10,3)}); // Time point at t = 10/3sec (fractional number) + CHECK (FSecs(10,3) == FSecs(10)/3); // fractional number arithmetics + CHECK (FSecs(10)/3 == 10_r/3); // _r is a user defined literal to denote 64-bit fractional + CHECK (10_r/3 == Rat(10,3)); + CHECK (Rat(10/3) == boost::rational(10,3)); // using Rat = boost::rational + CHECK (rational_cast (10_r/3) == 3.33333f); // rational_cast performs the division with indicated type + verify_simpleUsage(); + verify_setup(); + verify_calibration(); + verify_metric(); + verify_window(); + verify_scroll(); } @@ -82,23 +84,312 @@ namespace test { verify_simpleUsage() { ZoomWindow zoomWin; - CHECK (zoomWin.overallSpan() == TimeSpan(Time::ZERO, Time(FSecs(23)))); - CHECK (zoomWin.visible() == TimeSpan(Time::ZERO, Time(FSecs(23)))); + CHECK (zoomWin.overallSpan() == TimeSpan(_t(0), FSecs(23))); + CHECK (zoomWin.visible() == TimeSpan(_t(0), FSecs(23))); CHECK (zoomWin.px_per_sec() == 25); zoomWin.nudgeMetric(+1); CHECK (zoomWin.px_per_sec() == 50); - CHECK (zoomWin.visible() == TimeSpan(Time(FSecs(23,4)), FSecs(23,2))); - CHECK (zoomWin.overallSpan() == TimeSpan(Time::ZERO, Time(FSecs(23)))); + CHECK (zoomWin.visible() == TimeSpan(_t(23,4), FSecs(23,2))); + CHECK (zoomWin.overallSpan() == TimeSpan(_t(0), FSecs(23))); zoomWin.nudgeVisiblePos(-1); CHECK (zoomWin.px_per_sec() == 50); - CHECK (zoomWin.visible() == TimeSpan(Time::ZERO, FSecs(23,2))); - CHECK (zoomWin.overallSpan() == TimeSpan(Time::ZERO, Time(FSecs(23)))); + CHECK (zoomWin.visible() == TimeSpan(_t(0), FSecs(23,2))); + CHECK (zoomWin.overallSpan() == TimeSpan(_t(0), FSecs(23))); } - /** @test */ + /** @test verify the possible variations for initial setup of the zoom window + * - can be defined either the canvas duration, or an explicit extension + * given in pixels, or both + * - window extension, when given, defines the visible span + * - otherwise the whole canvas is visible, thereby defining the metric + */ + void + verify_setup() + { + ZoomWindow win1; + CHECK (win1.overallSpan() == TimeSpan(_t(0), FSecs(23))); + CHECK (win1.visible() == win1.overallSpan()); + CHECK (win1.px_per_sec() == 25); + CHECK (win1.pxWidth() == 23*25); + + ZoomWindow win2{TimeSpan{_t(-1), _t(+1)}}; + CHECK (win2.overallSpan() == TimeSpan(_t(-1), FSecs(2))); + CHECK (win2.visible() == win2.overallSpan()); + CHECK (win2.px_per_sec() == 25); + CHECK (win2.pxWidth() == 2*25); + + ZoomWindow win3{555}; + CHECK (win3.overallSpan() == TimeSpan(_t(0), FSecs(23))); + CHECK (win3.pxWidth() == 555); + CHECK (win3.px_per_sec() == 555_r/23); + CHECK (win3.visible() == win3.overallSpan()); + + ZoomWindow win4{555, TimeSpan{_t(-10), _t(-5)}}; + CHECK (win4.overallSpan() == TimeSpan(_t(10), FSecs(5))); + CHECK (win4.pxWidth() == 555); + CHECK (win4.px_per_sec() == 111); + CHECK (win4.visible() == win4.overallSpan()); + } + + + /** @test verify defining and retaining of 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, + * in which case the window is shifted, retaining metrics + * - however, if resulting window can not be made to fit, it is truncated + * to current canvas and metric is adjusted to keep overall pixel extension + */ + void + verify_calibration() + { + ZoomWindow win; + CHECK (win.overallSpan() == TimeSpan(_t(0), FSecs(23))); + CHECK (win.visible() == TimeSpan(_t(0), FSecs(23))); + CHECK (win.pxWidth() == 23*25); + + win.calibrateExtension(25); + CHECK (win.overallSpan() == TimeSpan(_t(0), FSecs(23))); + CHECK (win.visible() == TimeSpan(_t(0), FSecs(1))); + CHECK (win.px_per_sec() == 25); + CHECK (win.pxWidth() == 25); + + win.setOverallRange(TimeSpan{_t(-50), _t(50)}); + CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(100))); + CHECK (win.visible() == TimeSpan(_t(0), FSecs(1))); + CHECK (win.px_per_sec() == 25); + CHECK (win.pxWidth() == 25); + + win.calibrateExtension(100); + CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(100))); + CHECK (win.visible() == TimeSpan(_t(0), FSecs(4))); + CHECK (win.px_per_sec() == 25); + CHECK (win.pxWidth() == 100); + + win.setRanges (TimeSpan{_t(-50), _t(10)}, TimeSpan{_t(-10), FSecs(10)}); + CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(60))); + CHECK (win.visible() == TimeSpan(_t(-10), _t(0))); + CHECK (win.px_per_sec() == 10); + CHECK (win.pxWidth() == 100); + + win.calibrateExtension(500); + CHECK (win.overallSpan() == TimeSpan(_t(-50), FSecs(60))); + CHECK (win.visible() == TimeSpan(_t(-40), FSecs(50))); + CHECK (win.px_per_sec() == 10); + CHECK (win.pxWidth() == 500); + + win.setOverallDuration (Duration{FSecs(30)}); + CHECK (win.overallSpan() == TimeSpan(_t(-50), _t(-20))); + CHECK (win.visible() == TimeSpan(_t(-50), FSecs(30))); + CHECK (win.px_per_sec() == 500_r/30); + CHECK (win.pxWidth() == 500); + + win.calibrateExtension(300); + CHECK (win.overallSpan() == TimeSpan(_t(-50), _t(-20))); + CHECK (win.visible() == TimeSpan(_t(-50), FSecs(30)*3/5)); + CHECK (win.px_per_sec() == 500_r/30); + CHECK (win.pxWidth() == 300); + } + + + /** @test zoom in and out, thereby adjusting the metric + * - window extension in pixels is always retained + * - window is shifted when surpassing canvas bounds + * - metric is adjusted to keep excess window within pixel extension + * - otherwise zooming is centred around an anchor position, favouring centre + */ + void + verify_metric() + { + ZoomWindow win{1280, TimeSpan{_t(0), FSecs(64)}}; + CHECK (win.px_per_sec() == 20); + + win.nudgeMetric(+1); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + CHECK (win.visible() == TimeSpan(_t(-32,2), FSecs(32))); + CHECK (win.px_per_sec() == 40); + CHECK (win.pxWidth() == 1280); + + win.setVisiblePos(0.0); + CHECK (win.visible() == TimeSpan(_t(0), FSecs(32))); // zoom window moved to left side of overall range + + win.nudgeMetric(+15); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + CHECK (win.visible() == TimeSpan(_t(0), FSecs(32,32768))); // now anchor position is at left bound + CHECK (win.px_per_sec() == 40*32768); + CHECK (win.pxWidth() == 1200); + // Note: already getting close to the time grid... + CHECK (win.visible().end() == TimeValue(976)); + CHECK (Time(FSecs(32,32768)) == TimeValue(976)); + CHECK (rational_cast (32_r/32768 * Time::SCALE) == 976.5625); + + win.nudgeMetric(+1); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION); // further zoom has been capped at 2px per µ-tick + CHECK (win.visible() == TimeSpan(_t(0), FSecs(1280_r/ZOOM_MAX_RESOLUTION))); + CHECK (win.pxWidth() == 1280); + + win.nudgeMetric(+1); + CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION); + win.setMetric(10*ZOOM_MAX_RESOLUTION); + CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION); + + // so this is the deepest zoom possible.... + CHECK (win.visible().duration() == TimeValue(640)); + CHECK (TimeValue(640) == _t(1280,ZOOM_MAX_RESOLUTION)); + + // and this the absolutely smallest possible zoom window + win.calibrateExtension(2); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + CHECK (win.visible().duration() == TimeValue(1)); + CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION); + CHECK (win.pxWidth() == 2); + + win.calibrateExtension(1); + CHECK (win.visible().duration() == TimeValue(1)); // window is guaranteed to be non-empty + CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION / 2); // zoom scale has thus been lowered to prevent window from vanishing + CHECK (win.pxWidth() == 1); + + win.calibrateExtension(1280); + CHECK (win.visible().duration() == TimeValue(1280)); + CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION / 2); + CHECK (win.pxWidth() == 1280); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + + win.nudgeMetric(-5); + CHECK (win.visible().duration() == Duration{32 * FSecs(1280/Time::SCALE)}); + CHECK (win.px_per_sec() == ZOOM_MAX_RESOLUTION / 64); + CHECK (win.pxWidth() == 1280); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + + win.nudgeMetric(-12); + CHECK (win.visible() == win.overallSpan()); // zoom out stops at full canvas size + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + CHECK (win.px_per_sec() == 20); + CHECK (win.pxWidth() == 1280); + + // but canvas can be forcibly extended by »reverse zooming« + win.expandVisibleRange (TimeSpan{_t(60), _t(62)}); // zoom such as to bring current window at given relative position + CHECK (win.px_per_sec() == 20_r/64*2); // scale thus adjusted to reduce 64 sec to 2 sec (scale can be fractional!) + CHECK (win.visible().duration() == _t(64 * 32)); // zoom window has been inversely expanded by factor 64/2 == 32 + CHECK (win.visible() == win.overallSpan()); // zoom fully covers the expanded canvas + CHECK (win.overallSpan() == TimeSpan(_t(-1920), _t(128))); // and overall canvas has been expanded to embed the previous window + CHECK (win.overallSpan().duration() == _t(2048)); // ... at indicated relative position (2sec ⟼ 64sec, one window size before end) + + // metric can be explicitly set (e.g. 5px per sound sample) + win.setMetric (5 * 1_r/44100); + CHECK (win.pxWidth() == 1280); + CHECK (win.visible().duration() == Duration{FSecs(5,44100)*1280}); + CHECK (win.overallSpan().duration() == _t(2048)); + } + + + /** @test position and extension of the visible window can be set explicitly */ + void + verify_window() + { + ZoomWindow win{1280, TimeSpan{_t(0), FSecs(64)}}; + CHECK (win.visible() == win.overallSpan()); + CHECK (win.px_per_sec() == 20); + + win.setVisibleDuration (Duration{FSecs(23,30)}); + CHECK (win.visible().duration() == _t(23,30)); + CHECK (win.visible().start() == Time(FSecs(64)/2 - FSecs(23/30)/2)); // when zooming down from full range, zoom anchor is window centre + CHECK (win.px_per_sec() == 1280_r/23*30); + CHECK (win.pxWidth() == 1280); + + win.setVisibleRange (TimeSpan{_t(12), FSecs(16)}); + CHECK (win.visible() == TimeSpan(_t(12), _t(12+16))); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + CHECK (win.px_per_sec() == 1280_r/16); + CHECK (win.pxWidth() == 1280); + + win.setVisiblePos(_t(12)); // bring a specific position into sight + CHECK (win.visible().start() < _t(12)); // window is placed such as to enclose this desired position + CHECK (win.visible().duration() == _t(16)); // window size and metric not changed + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(64))); + CHECK (win.px_per_sec() == 1280_r/16); + CHECK (win.pxWidth() == 1280); + + win.setVisiblePos(0.80); // positioning relatively within overall canvas + CHECK (win.visible().start() < Time{FSecs(64)*8/10}); // window will enclose the desired anchor position + CHECK (win.visible().end() > Time{FSecs(64)*8/10}); + CHECK (win.px_per_sec() == 1280_r/16); + CHECK (win.pxWidth() == 1280); + + // manipulate canvas extension explicitly + win.setOverallDuration (Duration{FSecs(3600)}); + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(3600))); + CHECK (win.px_per_sec() == 1280_r/16); + CHECK (win.pxWidth() == 1280); + CHECK (win.visible().duration() == _t(16)); // window position and size not affected + CHECK (win.visible().start() < Time{FSecs(64)*8/10}); + CHECK (win.visible().end() > Time{FSecs(64)*8/10}); + + // reposition nominal canvas anchoring + win.setOverallRange (TimeSpan{_t(-64), _t(-32)}); + CHECK (win.overallSpan() == TimeSpan(_t(-64), FSecs(32))); // canvas nominally covers a completely different time range now + CHECK (win.px_per_sec() == 1280_r/16); // metric is retained + CHECK (win.pxWidth() == 1280); + CHECK (win.visible() == TimeSpan(_t(-32-16), FSecs(16))); // window scrolled left to remain within canvas + + win.setOverallStart (_t(100)); + CHECK (win.overallSpan() == TimeSpan(_t(100), FSecs(32))); + CHECK (win.visible() == TimeSpan(_t(100), FSecs(16))); // window scrolled right to remain within canvas + CHECK (win.px_per_sec() == 1280_r/16); // metric is retained + + win.setOverallRange (TimeSpan{_t(50), _t(52)}); + CHECK (win.overallSpan() == TimeSpan(_t(100), FSecs(2))); + CHECK (win.visible() == TimeSpan(_t(100), FSecs(16))); // window truncated to fit into canvas + CHECK (win.px_per_sec() == 1280_r/2); // metric need to be adjusted + CHECK (win.pxWidth() == 1280); + } + + + /** @test sliding the visible window, possibly expanding canvas */ + void + verify_scroll() + { + ZoomWindow win{1280, TimeSpan{_t(0), FSecs(16)}}; + CHECK (win.visible() == win.overallSpan()); + CHECK (win.visible() == TimeSpan(_t(0), FSecs(16))); + CHECK (win.px_per_sec() == 80); + + win.nudgeVisiblePos(+1); + CHECK (win.visible() == TimeSpan(_t(8), FSecs(16))); // window shifted forward by half a page + CHECK (win.overallSpan() == TimeSpan(_t(0), FSecs(16+8))); // canvas expanded accordingly + CHECK (win.px_per_sec() == 80); // metric is retained + CHECK (win.pxWidth() == 1280); + + win.nudgeVisiblePos(-3); + CHECK (win.visible() == TimeSpan(_t(-16), FSecs(16))); // window shifted backwards by three times half window sizes + CHECK (win.overallSpan() == TimeSpan(_t(-16), FSecs(16+8+32))); // canvas is always expanded accordingly, never shrinked + CHECK (win.px_per_sec() == 80); // metric is retained + CHECK (win.pxWidth() == 1280); + + win.setVisiblePos(0.50); + CHECK (win.visible() == TimeSpan(_t((56/2-16) -8), FSecs(16))); // window positioned to centre of canvas + + win.setVisiblePos(-0.50); + CHECK (win.visible() == TimeSpan(_t(-16), FSecs(16))); // relative positioning limited at lower bound + win.setVisiblePos(2.34); + CHECK (win.visible() == TimeSpan(_t(56-16-16), FSecs(16))); // relative positioning limited at upper bound + win.setVisiblePos(_t(200)); + CHECK (win.visible() == TimeSpan(_t(56-16-16), FSecs(16))); // absolute positioning limited likewise + win.setVisiblePos(_t(-200)); + CHECK (win.visible() == TimeSpan(_t(-16), FSecs(16))); + CHECK (win.px_per_sec() == 80); // metric retained + CHECK (win.pxWidth() == 1280); + + win.setVisibleRange(TimeSpan{_t(-200), FSecs(32)}); // but explicit positioning outside of canvas is possible + CHECK (win.overallSpan() == TimeSpan(_t(-200), _t(16+8))); // ...and will expand canvas + CHECK (win.visible() == TimeSpan(_t(-200), FSecs(32))); + CHECK (win.px_per_sec() == 40); + CHECK (win.pxWidth() == 1280); + } }; diff --git a/wiki/renderengine.html b/wiki/renderengine.html index 2aa8b327f..0001916bd 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -10505,18 +10505,20 @@ Wiring requests are small stateful value objects. They will be collected, sorted &rarr; ConManager -
+
//A component  for uniform handling of zoom scale and visible interval on the timeline.//
 Working with and arranging media requires a lot of //navigation// and changes of //zoom detail level.// More specifically, the editor is required to repeatedly return //to the same locations// and show arrangements at the same alternating scale levels. Most existing editing applications approach this topic naïvely, by just responding to some coarse grained interaction controls -- thereby creating the need for a lot of superfluous and tedious search and navigation activities, causing constant grind for the user. And resolving these obnoxious shortcomings turns out as a never ending task, precisely due to the naïve and ad hoc approach initially taken.
 
 Based on these observations, the design of the Lumiera UI calls for centralisation of all zoom- and navigation handling into a single component, instantiated once for every visible context, outfitted with the ability to capture and maintain a //history of zoom and navigation activities.// This component is called »''Zoom Window''«, since it represents a window-like local visible interval, embedded into a larger time span covering the whole timeline. The //current zoom state// is thus defined by
-* the overall {{{TimeSpan}}} of the timeline, including a start time (inclusive) and an end time (exclusive)
+* the overall {{{TimeSpan}}} of the timeline canvas, including a start time (inclusive) and an end time (exclusive)
 * the //visible interval// („window“), likewise modelled as {{{time::TimeSpan}}}
-* the //scale// defined as pixels per second {{red{10/2022 -- imposing a hard limit at 1px / sec ≙ 20min per screen page}}}
+* the //scale// defined as pixels per second
+** this is a //fractional value// -- allowing to specify multiples of frame sizes or sound sample durations
+** maximum scale limited to 2px per µ-tick; widest zoom limited to overall canvas duration
 
 !Requirement Analysis
 The Timeline UI is {{red{as of 10/2022}}} specified sufficiently to serve as framework to determine the requirements of a zoom handling component
-!!!User
+!!!Usages
 ;~CanvasHook + ~DisplayMetric
 :info | pull
 :*Pixel / sec
@@ -10601,10 +10603,18 @@ The Timeline UI is {{red{as of 10/2022}}} specified sufficiently to serve as fra
 #*translations
 #*window offset
 #*overall range
+#*resulting window extension in pixels
+
+!!!Semantics
+The {{{overallSpan}}} corresponds to the whole canvas extension
+The {{{visibleWindow}}} corresponds to the actually visible part, and the //metric// needs to be calibrated, to make the window's extension in pixel match the actual size in the UI. All further manipulations are assumed to keep that pixel extension constant. The actual duration of the timeline is in no way limited, but usually you'd expect the timeline to fit onto the canvas -- the canvas may even be extended when the user wants to extend the timeline beyond existing bounds.
 
 
 !Implementation
 The above requirement analysis reveals a common handling scheme, which is best served by a conventional design with an object, mutator and getter methods and encapsulated state. Moreover, a single client for push notification can be identified: the ~TimelineLayout (implementation of the [[DisplayManager interface|TimelineDisplayManager]]; it suffices to notify this collaboration partner; since at implementation level the ZoomWindow is itself embedded by mix-in into the ~TimelineLayout, the latter can easily read the resulting zoom parameters and react accordingly.
+
+Lumiera uses a µs-grid as base for the internal time representation {{red{11/2022 might revisit this decision, see #1258}}}, which generally is a good balance between performance and the addressable value range. However, for these calculations aimed at pixel precise drawing, this even this micro grid turns out to be not precise enough, especially for sample accurate sound automation. To work around this limitation, the ~ZoomWindow //metric// (scale factor) is represented as fractional number (implemented as {{{boost::rational<int64_t>}}}. This allows to carry out internal calculations involving scale factors with lossless integral arithmetics, and thus precisely to retain a given window extension in screen pixels.
+
 
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index e47b2108a..c2cb0cce8 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -24268,7 +24268,7 @@ - + @@ -38244,6 +38244,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ ...dann müssen wir die Vorgabe speichern, und in jedem Schritt korrigierend eingreifen +

+ +
+
+
+
@@ -38478,7 +38520,202 @@
+ + + + + + + + + + + + + + + + + + + + + + + + +
    +
  • + Canvas vergrößern ⟹ führt zu kontinuierliecher Drift; der Canvas wird fortlaufend größer +
  • +
  • + Window verkleinern ⟹ heißt daß man u.U den ganzen Canvas nicht ohne Rest darstellen kann +
  • +
+ +
+
+ + + +
+ + + + + + + + + + + + +

+ alle 20 Schritte ein Sprung, bzw. sogar nur alle 10 Schritte bei 96kHz, denn 1/96000 = 10.41666666 µTick +

+ +
+
+ + + + + + +

+ Die µ-Ticks hatten wir seinerzeit gewählt, weil sie einerseits hinreichend genau sind, andererseits sehr einfach zu implementieren, und dennoch die Darstellung extrem großer Zeitspannen ermöglichen +

+ +
+ + + + + + + + + + + +

+ Tatsächlich kann ein µTick-Grid auch Sound-Samples korrekt addressieren — man darf dann nur nicht diese Zeit-Werte für weitere Berechnungen verwenden (denn sonst sammeln sich Rundungsfehler an). Es könnte also eine Implementierung eben wissen, daß hier Sound-Samples dargestellt/verarbeitet werden, und intern mit der exakten Skala arbeiten. Im Grunde ist das ein Lösungsvorgriff auf die 3.Lösungsvariante (Problem ignorieren und per Metadaten tunneln)... siehe Diskussion in #1258 +

+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ user-defined Literal: _r +

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -55313,7 +55550,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Man würde also neben das Standard-Format ein toleriertes zweites Format stellen, welches dann ein Bürger zweiter Klasse wäre, aber auf allen wichtien APIs als 2.Alternative mit auftaucht. Zudem würde man gewisse Abkürzungs-Pfade schaffen, auf denen die alternative Spec dann verlustfrei durchgereicht werden kann. +

+
    +
  • + es ist überhaupt nicht klar, welches Format dann der Standardfall sein sollte +
  • +
  • + das läuft vor allem auf eine Performance-Betrachtung hinaus, und einen trade-off, wo man ggfs Fehler durch andere Programmierer akzeptiert +
  • +
+ +
+
+
+
+ + + + + + + + + +

+ wenn man zum µ-Grid eine eindeutige Rundungs-Regel hinzufügt, +

+

+ kann es praktisch auch Sound-Samples korrekt addressieren:

1/96000 ≙ 10,41666666666666666667 µTicks +

+ +
+ +
+ + + + + + + + + + + + + + + + +
+ @@ -65576,10 +65912,17 @@ - + + + + + + + +