diff --git a/src/stage/model/zoom-window.hpp b/src/stage/model/zoom-window.hpp index 8dbc49311..215c82c57 100644 --- a/src/stage/model/zoom-window.hpp +++ b/src/stage/model/zoom-window.hpp @@ -83,19 +83,14 @@ #include "lib/time/timevalue.hpp" #include "lib/nocopy.hpp" #include "lib/util.hpp" -//#include "lib/idi/entry-id.hpp" -//#include "lib/symbol.hpp" #include -//#include -//#include +#include namespace stage { namespace model { -// using std::string; -// using lib::Symbol; using lib::time::TimeValue; using lib::time::TimeSpan; using lib::time::Duration; @@ -187,6 +182,8 @@ namespace model { startWin_, afterWin_; Rat px_per_sec_; + std::function changeSignal_{}; + public: ZoomWindow (uint pxWidth, TimeSpan timeline =TimeSpan{Time::ZERO, DEFAULT_CANVAS}) : startAll_{timeline.start()} @@ -422,7 +419,29 @@ namespace model { } + /** Attach a λ or functor to be triggered on each actual change. */ + template + void + attachChangeNotification (FUN&& trigger) + { + changeSignal_ = std::forward (trigger); + } + + void + detachChangeNotification() + { + changeSignal_ = std::function(); + } + + private: + void + fireChangeNotification() + { + if (changeSignal_) changeSignal_(); + } + + /* === establish and maintain invariants === */ /* * - oriented and non-empty windows @@ -752,23 +771,9 @@ namespace model { posFactor = (posFactor + 1) / 2; // 0 ... 1 return posFactor; } - - - - void - fireChangeNotification() - { - TODO("really fire..."); - } }; - - /** */ - - - - }} // namespace stage::model #endif /*STAGE_MODEL_ZOOM_WINDOW_H*/ diff --git a/tests/stage/model/zoom-window-test.cpp b/tests/stage/model/zoom-window-test.cpp index 6b57e2ab7..6718b0117 100644 --- a/tests/stage/model/zoom-window-test.cpp +++ b/tests/stage/model/zoom-window-test.cpp @@ -22,6 +22,9 @@ /** @file zoom-window-test.cpp ** unit test \ref ZoomWindow_test + ** 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. */ @@ -81,6 +84,8 @@ namespace test { verify_metric(); verify_window(); verify_scroll(); + + verify_changeNotification(); } @@ -379,7 +384,7 @@ namespace test { 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.visible() == TimeSpan(_t(-16), FSecs(16))); // window shifted backwards by three times half window size CHECK (win.overallSpan() == TimeSpan(_t(-16), FSecs(16+8+16))); // canvas is always expanded accordingly, never shrinked CHECK (win.px_per_sec() == 80); // metric is retained CHECK (win.pxWidth() == 1280); @@ -400,6 +405,83 @@ namespace test { CHECK (win.px_per_sec() == 40); CHECK (win.pxWidth() == 1280); } + + + /** @test a notification-λ can be attached and will be triggered on each change */ + void + verify_changeNotification() + { + ZoomWindow win{100, TimeSpan{_t(0), FSecs(4)}}; + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(4))); + CHECK (win.visible() == TimeSpan(_t(0), _t(4))); + CHECK (win.px_per_sec() == 25); + CHECK (win.pxWidth() == 100); + + bool notified{false}; + win.nudgeMetric(+1); + CHECK (not notified); + CHECK (win.px_per_sec() == 50); + CHECK (win.visible().duration() == _t(2)); + + win.attachChangeNotification([&](){ notified = true; }); + CHECK (not notified); + CHECK (win.px_per_sec() == 50); + win.nudgeMetric(+1); + CHECK (win.px_per_sec() == 100); + CHECK (notified); + + notified = false; + CHECK (win.visible().start() == _t(3,2)); + win.nudgeVisiblePos(+1); + CHECK (win.visible().start() == _t(2)); + CHECK (notified); + + notified = false; + CHECK (win.overallSpan() == TimeSpan(_t(0), _t(4))); + win.setOverallRange(TimeSpan(_t(-4), _t(4))); + CHECK (win.overallSpan() == TimeSpan(_t(-4), _t(4))); + CHECK (notified); + + notified = false; + CHECK (win.pxWidth() == 100); + win.calibrateExtension(200); + CHECK (win.pxWidth() == 200); + CHECK (win.px_per_sec() == 100); + CHECK (notified); + + notified = false; + bool otherTriger{false}; + ZoomWindow wuz{10, TimeSpan{_t(0), FSecs(1)}}; + wuz.attachChangeNotification([&](){ otherTriger = true; }); + CHECK (wuz.visible().start() == _t(0)); + CHECK (not notified); + CHECK (not otherTriger); + wuz.nudgeVisiblePos(-1); + CHECK (not notified); + CHECK (otherTriger); + CHECK (wuz.visible().start() == _t(-1,2)); + + otherTriger = false; + CHECK (not notified); + win.nudgeMetric(+1); + CHECK (not otherTriger); + CHECK (notified); + CHECK (win.px_per_sec() == 200); + CHECK (wuz.px_per_sec() == 10); + + notified = false; + otherTriger = false; + win.detachChangeNotification(); + win.nudgeMetric(+1); + CHECK (not notified); + CHECK (win.px_per_sec() == 400); + + wuz.nudgeMetric(+1); + CHECK (not notified); + CHECK (otherTriger); + CHECK (win.px_per_sec() == 400); + CHECK (wuz.px_per_sec() == 20); + } }; diff --git a/wiki/renderengine.html b/wiki/renderengine.html index 0001916bd..2206e8adf 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -10505,7 +10505,7 @@ Wiring requests are small stateful value objects. They will be collected, sorted → 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.
 
@@ -10608,12 +10608,19 @@ The Timeline UI is {{red{as of 10/2022}}} specified sufficiently to serve as fra
 !!!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.
+;Invariants
+:after each change, a normalisation sequence is performed to (re)establish the following guarantees
+:* windows are always non-empy and properly oriented
+:* the given width in pixels is always retained
+:* zoom metric factor < max zoom (2px / µ-tick)
+:* visibleWindow ⊂ Canvas
+
 
 
 !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.
+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, 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. Since the temporal extension is still determined on a µs scale, sometimes the (fractional) zoom factor must be adjusted slightly to match the grid -- preference is always to reduce the factor or increase the window to the next tick.
 
 
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index a056e65dc..23d2ea3cd 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -38260,14 +38260,15 @@ - + + - + @@ -38283,6 +38284,38 @@

+ + + + + + + + + +

+ das heißt... +

+
    +
  • + alten Pixel-Wert berechnet +
  • +
  • + Neue Metrik draus per fractional-Integer-Arithmetik errechnet +
  • +
  • + Assertion daß sich daraus wieder die gleiche Pixel-Zahl ergibt +
  • +
+ +
+
+ + + + + +
@@ -38343,7 +38376,11 @@ - + + + + + @@ -38520,7 +38557,7 @@ - + @@ -38692,7 +38729,7 @@ - + @@ -39479,6 +39516,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +