Timeline: implement and verify ZoomWindow change notification

This commit is contained in:
Fischlurch 2022-11-11 16:30:27 +01:00
parent 3f396ef3b2
commit cc16953fd8
4 changed files with 192 additions and 28 deletions

View file

@ -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 <limits>
//#include <utility>
//#include <string>
#include <functional>
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<void()> 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<class FUN>
void
attachChangeNotification (FUN&& trigger)
{
changeSignal_ = std::forward<FUN> (trigger);
}
void
detachChangeNotification()
{
changeSignal_ = std::function<void()>();
}
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*/

View file

@ -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);
}
};

View file

@ -10505,7 +10505,7 @@ Wiring requests are small stateful value objects. They will be collected, sorted
&amp;rarr; ConManager
</pre>
</div>
<div title="ZoomWindow" creator="Ichthyostega" modifier="Ichthyostega" created="202210281528" modified="202211040231" tags="spec GuiPattern draft" changecount="18">
<div title="ZoomWindow" creator="Ichthyostega" modifier="Ichthyostega" created="202210281528" modified="202211111518" tags="spec GuiPattern" changecount="21">
<pre>//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 &lt; 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&lt;int64_t&gt;}}}. 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&lt;int64_t&gt;}}}. 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.
</pre>
</div>

View file

@ -38260,14 +38260,15 @@
<node CREATED="1667254209434" ID="ID_1299162840" MODIFIED="1667254216213" TEXT="st&#xf6;&#xdf;t am Rand des Canvas an"/>
</node>
</node>
<node CREATED="1667254983055" ID="ID_1939856323" MODIFIED="1667254987549" TEXT="Eichung der Metrik">
<node COLOR="#338800" CREATED="1667254983055" FOLDED="true" ID="ID_1939856323" MODIFIED="1668176401891" TEXT="Eichung der Metrik">
<icon BUILTIN="button_ok"/>
<node CREATED="1667254988850" ID="ID_627529918" MODIFIED="1667255165088" TEXT="mu&#xdf; (initial) explizit gesezt werden">
<icon BUILTIN="info"/>
</node>
<node CREATED="1667255003826" ID="ID_457439715" MODIFIED="1667255160405" TEXT="soll der Ausdehnung im Interface entsprechen">
<icon BUILTIN="yes"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1667255062416" ID="ID_888315158" MODIFIED="1667255155045" TEXT="Erwartung: erh&#xe4;lt sich von selbst numerisch konstant">
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1667255062416" ID="ID_888315158" MODIFIED="1668176391632" TEXT="Erwartung: erh&#xe4;lt sich von selbst numerisch konstant">
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1667255198638" ID="ID_1458374199" MODIFIED="1667255216695" TEXT="da wir Integer-Arithmetik machen"/>
<node CREATED="1667255218491" ID="ID_1646057612" MODIFIED="1667255227637" TEXT="und stets logisch-konsistent vorgehen"/>
@ -38283,6 +38284,38 @@
</p>
</body>
</html></richcontent>
<node CREATED="1668176223375" ID="ID_386415995" MODIFIED="1668176380384" TEXT="...habe es gar nicht darauf ankommen lassen">
<icon BUILTIN="yes"/>
</node>
<node CREATED="1668176234367" ID="ID_762072729" MODIFIED="1668176354941" TEXT="...sondern an allen potentiell gef&#xe4;hrlichen Rechenschritten explizit auf Wert-Erhalt abgestellt">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
das hei&#223;t...
</p>
<ul>
<li>
alten Pixel-Wert berechnet
</li>
<li>
Neue Metrik draus per fractional-Integer-Arithmetik errechnet
</li>
<li>
Assertion da&#223; sich daraus wieder die gleiche Pixel-Zahl ergibt
</li>
</ul>
</body>
</html></richcontent>
</node>
<node COLOR="#435e98" CREATED="1668176355944" ID="ID_421203855" MODIFIED="1668176376274" TEXT="Pixel-Wert selber mu&#xdf; nicht persistent gespeichert werden">
<icon BUILTIN="idea"/>
</node>
<node COLOR="#338800" CREATED="1668176368364" ID="ID_1536037495" MODIFIED="1668176373232" TEXT="im Test best&#xe4;tigt">
<icon BUILTIN="button_ok"/>
</node>
</node>
</node>
</node>
@ -38343,7 +38376,11 @@
<node COLOR="#435e98" CREATED="1666912636672" ID="ID_693136797" MODIFIED="1666966124725" TEXT="wie kann man ZoomWindow nutzen?">
<icon BUILTIN="help"/>
<node CREATED="1666912660076" ID="ID_1930000724" MODIFIED="1666912681102" TEXT="dynamische &#xc4;nderungen vornehmen"/>
<node CREATED="1666912682282" ID="ID_1683687274" MODIFIED="1666966070412" TEXT="&#xc4;nderungs-Benachrichtigung (Listener)"/>
<node CREATED="1666912682282" ID="ID_1683687274" MODIFIED="1666966070412" TEXT="&#xc4;nderungs-Benachrichtigung (Listener)">
<node CREATED="1668176481848" ID="ID_1029152707" MODIFIED="1668176485296" TEXT="einfacher Trigger"/>
<node CREATED="1668176485655" ID="ID_1241434698" MODIFIED="1668176499366" TEXT="Empf&#xe4;nger zieht sich selber die aktuellen Werte"/>
<node CREATED="1668176501266" ID="ID_1187154193" MODIFIED="1668176520856" TEXT="vorerst nur ein Empf&#xe4;nger (=TimelineLayout)"/>
</node>
</node>
<node COLOR="#435e98" CREATED="1666913019821" ID="ID_1177711704" MODIFIED="1666966123770" TEXT="Art der Anbindung?">
<icon BUILTIN="help"/>
@ -38520,7 +38557,7 @@
</node>
</node>
</node>
<node COLOR="#338800" CREATED="1667260101665" ID="ID_1250580560" MODIFIED="1668132037114" TEXT="mu&#xdf; die Ausdehnung in Pixel beachten">
<node COLOR="#338800" CREATED="1667260101665" FOLDED="true" ID="ID_1250580560" MODIFIED="1668132037114" TEXT="mu&#xdf; die Ausdehnung in Pixel beachten">
<icon BUILTIN="button_ok"/>
<node COLOR="#338800" CREATED="1667260119686" ID="ID_1154266769" MODIFIED="1667780434377" TEXT="neuer Getter daf&#xfc;r">
<icon BUILTIN="button_ok"/>
@ -38692,7 +38729,7 @@
</node>
</node>
</node>
<node COLOR="#338800" CREATED="1667603646312" ID="ID_1507636517" MODIFIED="1668132008216" TEXT="mu&#xdf; Invarianten sicherstellen">
<node COLOR="#338800" CREATED="1667603646312" FOLDED="true" ID="ID_1507636517" MODIFIED="1668132008216" TEXT="mu&#xdf; Invarianten sicherstellen">
<linktarget COLOR="#5c3488" DESTINATION="ID_1507636517" ENDARROW="Default" ENDINCLINATION="27;-47;" ID="Arrow_ID_49426086" SOURCE="ID_162164091" STARTARROW="None" STARTINCLINATION="-122;6;"/>
<icon BUILTIN="yes"/>
<node COLOR="#6a1790" CREATED="1667603900358" ID="ID_353785530" MODIFIED="1667780480867" TEXT="das gew&#xe4;hlte Implementierungs-Schema ist nicht beherrschbar">
@ -39479,6 +39516,39 @@
</node>
</node>
</node>
<node COLOR="#338800" CREATED="1668176529007" ID="ID_1000385499" MODIFIED="1668178932385" TEXT="changeNotification">
<icon BUILTIN="button_ok"/>
<node CREATED="1668176558035" ID="ID_389422615" MODIFIED="1668176565812" TEXT="es ist ein reiner Trigger">
<icon BUILTIN="idea"/>
</node>
<node COLOR="#338800" CREATED="1668176566961" ID="ID_1163344157" MODIFIED="1668178900570" TEXT="also gen&#xfc;gt es, zu beweisen, da&#xdf; er ausgel&#xf6;st wird">
<icon BUILTIN="button_ok"/>
</node>
<node COLOR="#338800" CREATED="1668178902040" ID="ID_1008564979" MODIFIED="1668178918007" TEXT="au&#xdf;erdem: Notification ist pro Instanz gesondert">
<icon BUILTIN="button_ok"/>
</node>
<node COLOR="#338800" CREATED="1668178918696" ID="ID_958127282" MODIFIED="1668178931317" TEXT="und man kann sie auch wieder abkoppeln">
<icon BUILTIN="button_ok"/>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668180004015" ID="ID_1355163433" MODIFIED="1668180133079" TEXT="extreme Grenzf&#xe4;lle abtesten">
<icon BUILTIN="flag-yellow"/>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668180016155" ID="ID_506165727" MODIFIED="1668180136076" TEXT="leere Intervalle">
<icon BUILTIN="flag-yellow"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668180071412" ID="ID_1188877277" MODIFIED="1668180136076" TEXT="falsch orientierte Intervalle">
<icon BUILTIN="flag-yellow"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668180030777" ID="ID_1319796356" MODIFIED="1668180136077" TEXT="1 &#xb5;-Tick">
<icon BUILTIN="flag-yellow"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668180054302" ID="ID_1604190635" MODIFIED="1668180136078" TEXT="1 Pixel">
<icon BUILTIN="flag-yellow"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668180120032" ID="ID_346213896" MODIFIED="1668180136079" TEXT="Grenzen des Zeitformats">
<icon BUILTIN="flag-yellow"/>
</node>
</node>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1667488193842" ID="ID_1347640673" MODIFIED="1667488208549" TEXT="Invarianten">