Timeline: implement rule for relative ZoomWindow positioning

This function allows to move the visible range such that it contains
a given time position; the relative location of this point within
the visible range however is in turn determined by relating it to
the current overall canvas: if we are close to the beginning, the
position is also located rather to the left side, and if we're
approaching the canvas end, the position tends to the right side...

(and yes, I am aware that the variant taking a rational number
can be derailed by causing internal numeric overflow, when passing
a maliciously crafted rational number, like INT_MAX-1 / INT_MAX )
This commit is contained in:
Fischlurch 2022-11-08 02:55:38 +01:00
parent a9df13f078
commit 29f030d2a1
2 changed files with 138 additions and 25 deletions

View file

@ -318,19 +318,35 @@ namespace model {
fireChangeNotification();
}
/**
* explicitly set the visible window,
* possibly expanding the canvas to fit.
* Typically used to zoom into a user selected range.
*/
void
setVisibleRange (TimeSpan newWindow)
{
mutateWindow (newWindow);
fireChangeNotification();
}
/**
* the »reverse zoom operation«: zoom out such as to
* bring the current window at the designated time span.
* Typically the user selects a sub-range, and the current
* view is then collapsed accordingly to fit into that range.
* As a side effect, the canvas may be expanded significantly.
*/
void
expandVisibleRange (TimeSpan target)
{
UNIMPLEMENTED ("zoom out to bring the current window at the designated time span");
}
/**
* explicitly set the duration of the visible window range,
* while keeping the start position; possibly expand canvas.
*/
void
setVisibleDuration (Duration duration)
{
@ -338,24 +354,15 @@ namespace model {
fireChangeNotification();
}
void
setVisiblePos (TimeValue posToShow)
{
UNIMPLEMENTED ("setVisiblePos");
}
void
setVisiblePos (float percentage)
{
UNIMPLEMENTED ("setVisiblePos");
}
/** scroll by arbitrary offset, possibly expanding canvas. */
void
offsetVisiblePos (Offset offset)
{
UNIMPLEMENTED ("offsetVisiblePos");
mutateWindow (TimeSpan{startWin_+offset, Duration{afterWin_-startWin_}});
fireChangeNotification();
}
/** scroll by increments of half window size, possibly expanding. */
void
nudgeVisiblePos (int steps)
{
@ -364,6 +371,35 @@ namespace model {
, dur});
}
/**
* scroll the window to bring the denoted position in sight,
* retaining the current zoom factor, possibly expanding canvas.
*/
void
setVisiblePos (Time posToShow)
{
FSecs canvasOffset{posToShow - startAll_};
anchorWindowAtPosition (canvasOffset);
fireChangeNotification();
}
/** scroll to reveal position designated relative to overall canvas */
void
setVisiblePos (Rat percentage)
{
FSecs canvasDuration{afterAll_-startAll_};
anchorWindowAtPosition (canvasDuration*percentage);
fireChangeNotification();
}
void
setVisiblePos (double percentage)
{ // use some arbitrary yet significantly large work scale
int64_t scale = max (_raw(afterAll_-startAll_), MAX_PX_WIDTH);
Rat factor{int64_t(scale*percentage), scale};
setVisiblePos (factor);
}
void
navHistory()
{
@ -432,7 +468,7 @@ namespace model {
timeDur = timeDur + TimeValue(1);
// resize window relative to anchor point
placeWindowRelativeToAnchor (dur);
changeWindowDuration (timeDur);
establishWindowDuration (timeDur);
// re-check metric to maintain precise pxWidth
px_per_sec_ = conformMetricToWindow (pxWidth);
ENSURE (_FSecs(afterWin_-startWin_) < MAX_TIMESPAN);
@ -522,13 +558,17 @@ namespace model {
ensureInvariants();
}
/** @internal change Window TimeSpan, validate and adjust all params */
/** @internal change Window TimeSpan, possibly also outside
* of the current canvas, which is then expanded;
* validate and adjust all params accordingly */
void
mutateWindow (TimeSpan window)
{
uint px{pxWidth()};
startWin_ = window.start();
afterWin_ = ensureNonEmpty (startWin_, window.end());
startAll_ = min (startAll_, startWin_);
afterAll_ = max (afterAll_, afterWin_);
px_per_sec_ = conformMetricToWindow (px);
ensureInvariants (px);
}
@ -592,11 +632,38 @@ namespace model {
{
pxWidth = util::limited (1u, pxWidth, MAX_PX_WIDTH);
FSecs adaptedWindow{Rat{pxWidth} / px_per_sec_};
changeWindowDuration (adaptedWindow);
establishWindowDuration (adaptedWindow);
px_per_sec_ = conformMetricToWindow (pxWidth);
ensureInvariants (pxWidth);
}
/** @internal relocate window anchored at a position relative to canvas,
* also placing the anchor position relative within the window
* in accordance with the position relative to canvas.
* Window will enclose the given position, possibly extending
* canvas to fit, afterwards reestablishing all invariants. */
void
anchorWindowAtPosition (FSecs canvasOffset)
{
REQUIRE (afterWin_ > startWin_);
REQUIRE (afterAll_ > startAll_);
uint px{pxWidth()};
FSecs duration{afterWin_-startWin_};
Rat posFactor = canvasOffset / FSecs{afterAll_-startAll_};
posFactor = parabolicAnchorRule (posFactor); // also limited 0...1
FSecs partBeforeAnchor = posFactor * duration;
startWin_ = startAll_ + (canvasOffset - partBeforeAnchor);
establishWindowDuration (duration);
startAll_ = min (startAll_, startWin_);
afterAll_ = max (afterAll_, afterWin_);
px_per_sec_ = conformMetricToWindow (px);
ensureInvariants (px);
}
/** @internal similar operation as #anchorWindowAtPosition(),
* but based on the current window position and without
* relocation, rather intended for changing the scale */
void
placeWindowRelativeToAnchor (FSecs duration)
{
@ -605,7 +672,7 @@ namespace model {
}
void
changeWindowDuration (TimeVar duration)
establishWindowDuration (TimeVar duration)
{
if (startWin_<= Time::MAX - duration)
afterWin_ = startWin_ + duration;
@ -651,12 +718,28 @@ namespace model {
// use a 3rd degree parabola to favour positions in the middle
Rat posFactor = FSecs{startWin_-startAll_} / possibleRange;
return parabolicAnchorRule (posFactor);
}
/**
* A counter movement rule to place an anchor point, based on a percentage factor.
* Used to define the anchor point within the window, depending on the window's position
* relative to the overall canvas. Implemented using a cubic parabola, which moves quick
* away from the boundaries, while hovering most of the time in the middle area.
* @return factor effectively between 0 ... 1 (inclusive)
*/
static Rat
parabolicAnchorRule (Rat posFactor)
{
posFactor = util::limited (0, posFactor, 1);
posFactor = (2*posFactor - 1); // -1 ... +1
posFactor = posFactor*posFactor*posFactor; // -1 ... +1 but accelerating towards boundaries
posFactor = (posFactor + 1) / 2; // 0 ... 1
return posFactor;
}
void
fireChangeNotification()
{

View file

@ -39389,16 +39389,46 @@
<icon BUILTIN="flag-yellow"/>
<node CREATED="1667528185832" ID="ID_1091652690" MODIFIED="1667528199122" TEXT="schrittweise incl Canvas erweitern"/>
<node CREATED="1667528199785" ID="ID_1692959674" MODIFIED="1667528207840" TEXT="limits bei relativem positionieren"/>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1667787116332" ID="ID_538942533" MODIFIED="1667787124519" TEXT="relativ nach Position und Prozentsatz">
<icon BUILTIN="flag-yellow"/>
<node COLOR="#338800" CREATED="1667787116332" ID="ID_538942533" MODIFIED="1667871911986" TEXT="relativ nach Position und Prozentsatz">
<icon BUILTIN="button_ok"/>
<node COLOR="#435e98" CREATED="1667871912848" ID="ID_28548966" MODIFIED="1667871963481" TEXT="das l&#xe4;uft auf eine Variange des relativen-Ankers hinaus">
<icon BUILTIN="idea"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1667528209948" ID="ID_914020436" MODIFIED="1667787027641" TEXT="explizit setzen und Canvas erweitern">
<icon BUILTIN="flag-yellow"/>
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1667787036706" ID="ID_192598582" MODIFIED="1667787077769" TEXT="mu&#xdf; hierf&#xfc;r die Limitierung explizit aufheben">
<node CREATED="1667871926941" ID="ID_56646265" MODIFIED="1667872010814" TEXT="kann aber nur die kubische Positions-Regel wiederverwenden...">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
alles Andere ist in diesem Fall eben doch speziell; wir kommen bereits mit einer relativen Angabe, und die aktuelle Fensterposition spielt keine Rolle
</p>
</body>
</html></richcontent>
</node>
</node>
<node COLOR="#338800" CREATED="1667528209948" ID="ID_914020436" MODIFIED="1667871908657" TEXT="explizit setzen und Canvas erweitern">
<icon BUILTIN="button_ok"/>
<node COLOR="#435e98" CREATED="1667787036706" ID="ID_192598582" MODIFIED="1667871690775" TEXT="mu&#xdf; hierf&#xfc;r die Limitierung explizit aufheben">
<icon BUILTIN="yes"/>
</node>
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#990033" CREATED="1667787054759" ID="ID_280714694" MODIFIED="1667787063999" TEXT="auf welcher Ebene am Besten einbauen?">
<icon BUILTIN="help"/>
<node CREATED="1667787054759" ID="ID_280714694" MODIFIED="1667871751969" TEXT="neue Festlegung: alle mutateWindow()-Aufrufe k&#xf6;nnen Canvas erweitern">
<icon BUILTIN="idea"/>
</node>
<node CREATED="1667871761244" ID="ID_550353046" MODIFIED="1667871901718" TEXT="hierzu &#xe4;ndere ich die Spezifikation: auch relatives Zoomen kann nun Canvas erweitern">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
warum auch nicht?
</p>
<p>
Das ist dann so viel konsistenter, insofern nun n&#228;mlich genau die Funktionen, die die visible-Window-Position setzen, auch den Canvas erweitern d&#252;rfen &#8212; aber alle anderen Funktionen sto&#223;en an der Canvas-Grenze an
</p>
</body>
</html></richcontent>
</node>
</node>
</node>