Timeline: ZoomWindow implementation draft

implement the first test case: nudge the zoom factor
⟹ scale factor doubled
⟹ visible window reduced to half size
⟹ visible window placed in the middle of the overall range
This commit is contained in:
Fischlurch 2022-10-30 01:31:25 +02:00
parent b3fe6e16c6
commit 7145d0d9ce
6 changed files with 255 additions and 92 deletions

View file

@ -102,7 +102,7 @@ extern "C" { /* ===================== C interface ======================== */
* as zero. Each interval includes its lower bound, but excludes its upper bound.
* @param grid spacing of the grid intervals, measured in GAVL_TIME_SCALE
* @return number of the grid interval containing the given time.
* @warning the resulting value is limited to (Time::Min, Time::MAX)
* @warning the resulting value is limited to (Time::Min, Time::MAX)
*/
int64_t
lumiera_quantise_frames (gavl_time_t time, gavl_time_t origin, gavl_time_t grid);
@ -112,12 +112,12 @@ lumiera_quantise_frames_fps (gavl_time_t time, gavl_time_t origin, uint framerat
/**
* Similar to #lumiera_quantise_frames, but returns a grid aligned \em time value
* @return time of start of the grid interval containing the given time,
* @return time of start of the grid interval containing the given time,
* but measured relative to the origin
* @warning because the resulting value needs to be limited to fit into a 64bit long,
* the addressable time range can be considerably reduced. For example, if
* origin = Time::MIN, then all original time values above zero will be
* clipped, because the result, relative to origin, needs to be <= Time::MAX
* clipped, because the result, relative to origin, needs to be <= Time::MAX
*/
gavl_time_t
lumiera_quantise_time (gavl_time_t time, gavl_time_t origin, gavl_time_t grid);

View file

@ -23,7 +23,7 @@
* *****************************************************/
/** @file time.cpp
/** @file time.cpp
** Lumiera time handling core implementation unit.
** This translation unit generates code for the Lumiera internal time wrapper,
** based on gavl_time_t, associated constants, marker classes for the derived
@ -32,7 +32,7 @@
**
** Client code includes either time.h (for basics and conversion functions)
** or timevalue.hpp (for the time entities), timequant.hpp for grid aligned
** time values or timecode.hpp
** time values or timecode.hpp
**
** @see Time
** @see TimeValue
@ -301,7 +301,7 @@ lumiera_tmpbuf_print_time (gavl_time_t time)
char *buffer = lumiera_tmpbuf_snprintf(64, "%s%01d:%02d:%02d.%03d",
negative ? "-" : "", hours, minutes, seconds, milliseconds);
ENSURE(buffer != NULL);
return buffer;
}
@ -352,7 +352,7 @@ namespace { // implementation: basic frame quantisation....
const int64_t limit_den = std::numeric_limits<gavl_time_t>::max() / framerate_divisor;
const int64_t microScale {lib::time::TimeValue::SCALE};
// protect against numeric overflow
// protect against numeric overflow
if (abs(time) < limit_num && microScale < limit_den)
{
// safe to calculate "time * framerate"
@ -393,7 +393,7 @@ lumiera_quantise_time (gavl_time_t time, gavl_time_t origin, gavl_time_t grid)
gavl_time_t
lumiera_time_of_gridpoint (int64_t nr, gavl_time_t origin, gavl_time_t grid)
{
gavl_time_t offset = nr * grid;
gavl_time_t offset = nr * grid;
return origin + offset;
}
@ -462,12 +462,12 @@ namespace { // implementation helper
const uint FRAMES_PER_10min = 10*60 * 30000/1001;
const uint FRAMES_PER_1min = 1*60 * 30000/1001;
const uint DISCREPANCY = (1*60 * 30) - FRAMES_PER_1min;
/** reverse the drop-frame calculation
* @param time absolute time value in micro ticks
* @param time absolute time value in micro ticks
* @return the absolute frame number using NTSC drop-frame encoding
* @todo I doubt this works correct for negative times!!
* @todo I doubt this works correct for negative times!!
*/
inline int64_t
calculate_drop_frame_number (gavl_time_t time)
@ -479,7 +479,7 @@ namespace { // implementation helper
// ensure the drop-frame incidents happen at full minutes;
// at start of each 10-minute segment *no* drop incident happens,
// thus we need to correct discrepancy between nominal/real framerate once:
// thus we need to correct discrepancy between nominal/real framerate once:
int64_t remainingMinutes = (tenMinFrames.rem - DISCREPANCY) / FRAMES_PER_1min;
int64_t dropIncidents = (10-1) * tenMinFrames.quot + remainingMinutes;
@ -523,6 +523,6 @@ lumiera_build_time_ntsc_drop (uint frames, uint secs, uint mins, uint hours)
gavl_time_t result = lumiera_framecount_to_time (total_frames, FrameRate::NTSC);
if (0 != result) // compensate for truncating down on conversion
result += 1; // without this adjustment the frame number
result += 1; // without this adjustment the frame number
return result; // would turn out off by -1 on back conversion
}

View file

@ -23,14 +23,47 @@
/** @file timevalue.hpp
** a family of time value like entities and their relationships.
** This is the foundation for the Lumiera time handling framework. On the implementation
** level, time values are represented as 64bit integer values \c gavl_time_t. But for the
** level, time values are represented as 64bit integer values `gavl_time_t`. But for the
** actual use, we create several kinds of time "values", based on their logical properties.
** These time values are considered to be fixed (immutable) values, which may only be
** created through some well defined construction paths, and any time based calculation
** created through some limited construction paths, and any time based calculation
** is forced to go through our time calculation library. This is prerequisite for
** the definition of <i>frame aligned</i> time values and time code representation
** the definition of _frame aligned_ time values and time code representation
** implemented as display format based on these frame quantised time values.
**
** # Time entities
**
** The value types defined in this header represent time points and time intervals
** based on an internal time scale (µs ticks) and not related to any known fixed time
** zone or time base; rather they are interpreted in usage context, and the only way
** to retrieve such a value is by formatting it into a time code format.
**
** The lib::time::TimeValue serves as foundation for all further time calculations;
** in fact it is implemented as a single 64bit µ-tick value (`gavl_time_t`). The
** further time entities are implemented as value objects (without virtual functions):
** - lib::time::Time represents a time instant and is the reference for any usage
** - lib::time::TimeVar is a mutable time variable and can be used for calculations
** - lib::time::Offset can be used to express a positive or negative shift on time scale
** - lib::time::Duration represents the extension or an amount of time
** - lib::time::TimeSpan represents a distinct interval, with start time and duration
** - lib::time::FrameRate can be used to mark a number to denote a frames-per-second spec
** - lib::time::FSecs is a rational number to represent seconds or fractions thereof
**
** # Manipulating time values
**
** Time values are conceived as fixed, immutable entities, similar to numbers; you can't
** just change the number two, and likewise, two seconds are two seconds. However, for
** many use cases we have to combine time values to perform calculations
** - Time entities can be combined with operators, to form new time entities
** - the TimeVar can be used as accumulator or variable for ongoing calculations
** - since TimeSpan, Duration (and the grid-aligned, "quantised" flavours) will often
** represent some time-like property or entity, e.g. the temporal specification of
** a media Clip with start and duration, there is the concept of an explicit *mutation*,
** which is _accepted_ by these entities. Notably the lib::time::Control can be attached
** to these entities, and can then receive manipulations (nudging, offset); moreover it
** is possible to attach as listener to such a "controller" and be notified by any
** manipulation; this setup is the base for running time display, playback cursors etc.
**
** @see time.h basic time calculation library functions
** @see timequant.hpp
** @see TimeValue_test
@ -75,7 +108,7 @@ namespace time {
* @note clients should prefer to use Time instances,
* which explicitly denote an Lumiera internal
* time value and are easier to use.
* @see TimeVar when full arithmetics are required
* @see TimeVar when full arithmetics are required
*/
class TimeValue
: boost::totally_ordered<TimeValue,
@ -107,7 +140,7 @@ namespace time {
static gavl_time_t limited (gavl_time_t raw);
explicit
explicit
TimeValue (gavl_time_t val=0) ///< time given in µ ticks here
: t_(limited (val))
{ }
@ -134,58 +167,6 @@ namespace time {
/** a mutable time value,
* behaving like a plain number,
* allowing copy and re-accessing
* @note supports scaling by a factor,
* which \em deliberately is chosen
* as int, not gavl_time_t, because the
* multiplying of times is meaningless.
*/
class TimeVar
: public TimeValue
, boost::additive<TimeVar,
boost::additive<TimeVar, TimeValue,
boost::multipliable<TimeVar, int>
> >
{
public:
TimeVar (TimeValue const& time = TimeValue())
: TimeValue(time)
{ }
// Allowing copy and assignment
TimeVar (TimeVar const& o)
: TimeValue(o)
{ }
TimeVar&
operator= (TimeValue const& o)
{
t_ = TimeVar(o);
return *this;
}
// Support mixing with plain long int arithmetics
operator gavl_time_t () const { return t_; }
// Supporting additive
TimeVar& operator+= (TimeVar const& tx) { t_ += tx.t_; return *this; }
TimeVar& operator-= (TimeVar const& tx) { t_ -= tx.t_; return *this; }
// Supporting multiplication with integral factor
TimeVar& operator*= (int64_t fact) { t_ *= fact; return *this; }
// Supporting sign flip
TimeVar operator- () const { return TimeVar(*this)*=-1; }
// baseclass TimeValue is already totally_ordered
};
@ -208,7 +189,65 @@ namespace time {
typedef boost::rational<int64_t> FSecs;
/**
/** a mutable time value,
* behaving like a plain number,
* allowing copy and re-accessing
* @note supports scaling by a factor,
* which _deliberately_ is chosen
* as int, not gavl_time_t, because the
* multiplying of times is meaningless.
*/
class TimeVar
: public TimeValue
, boost::additive<TimeVar,
boost::additive<TimeVar, TimeValue,
boost::multipliable<TimeVar, int>
> >
{
public:
TimeVar (TimeValue const& time = TimeValue())
: TimeValue(time)
{ }
/** Allow to pick up precise fractional seconds
* @warning truncating fractional µ-ticks */
TimeVar (FSecs const&);
/// Allowing copy and assignment
TimeVar (TimeVar const& o)
: TimeValue(o)
{ }
TimeVar&
operator= (TimeValue const& o)
{
t_ = TimeVar(o);
return *this;
}
/// Support mixing with plain long int arithmetics
operator gavl_time_t() const { return t_; }
/// Support for micro-tick precise time arithmetics
operator FSecs() const { return FSecs{t_, TimeValue::SCALE}; }
/// Supporting additive
TimeVar& operator+= (TimeVar const& tx) { t_ += tx.t_; return *this; }
TimeVar& operator-= (TimeVar const& tx) { t_ -= tx.t_; return *this; }
/// Supporting multiplication with integral factor
TimeVar& operator*= (int64_t fact) { t_ *= fact; return *this; }
/// Supporting sign flip
TimeVar operator- () const { return TimeVar(*this)*=-1; }
// baseclass TimeValue is already totally_ordered
};
/**********************************************************//**
* Lumiera's internal time value datatype.
* This is a TimeValue, but now more specifically denoting
* a point in time, measured in reference to an internal
@ -216,7 +255,7 @@ namespace time {
*
* Lumiera Time provides some limited capabilities for
* direct manipulation; Time values can be created directly
* from \c (ms,sec,min,hour) specification and there is an
* from `(ms,sec,min,hour)` specification and there is an
* string representation intended for internal use (reporting
* and debugging). Any real output, formatting and persistent
* storage should be based on the (quantised) timecode
@ -291,14 +330,14 @@ namespace time {
* but derived classes allow some limited mutation
* through special API calls */
Offset&
operator= (Offset const& o)
operator= (Offset const& o)
{
TimeValue::operator= (o);
return *this;
}
public:
explicit
explicit
Offset (TimeValue const& distance =Time::ZERO)
: TimeValue(distance)
{ }
@ -367,7 +406,7 @@ namespace time {
inline Offset
Offset::operator- () const
{
return -1 * (*this);
return -1 * (*this);
}
@ -377,8 +416,8 @@ namespace time {
* Duration is the internal Lumiera time metric.
* It is an absolute (positive) value, but can be
* promoted from an offset. While Duration generally
* is treated as immutable value, there is the
* possibility to send a \em Mutation message.
* is treated as immutable value, there is the
* possibility to send a _Mutation message_.
*/
class Duration
: public TimeValue
@ -446,7 +485,7 @@ namespace time {
inline Offset
Duration::operator- () const
{
return -1 * (*this);
return -1 * (*this);
}
@ -500,13 +539,13 @@ namespace time {
Duration&
duration()
duration()
{
return dur_;
}
Duration
duration() const
duration() const
{
return dur_;
}
@ -607,11 +646,16 @@ namespace time {
inline gavl_time_t
TimeValue::limited (gavl_time_t raw)
{
return raw > Time::MAX? Time::MAX.t_
: raw < Time::MIN? Time::MIN.t_
return raw > Time::MAX? Time::MAX.t_
: raw < Time::MIN? Time::MIN.t_
: raw;
}
inline
TimeVar::TimeVar (FSecs const& fractionalSeconds)
: TimeVar{Time(fractionalSeconds)}
{ }
inline
Duration::Duration (TimeSpan const& interval)
: TimeValue(interval.duration())
@ -645,7 +689,7 @@ namespace time {
namespace util {
inline bool
isnil (lib::time::Duration const& dur)
{

View file

@ -22,14 +22,17 @@
/** @file zoom-window.hpp
** Abstraction: a multi-dimensional extract from model space into screen coordinates.
** This is a generic component to represent and handle the zooming and positioning of
** views within an underlying model space. This model space is conceived to be two fold:
** Abstraction: the current zoom- and navigation state of a view, possibly in multiple
** dimensions. This is a generic component to represent and handle the zooming and
** positioning of views within an underlying model space. This model space is conceived
** to be two fold:
** - it is a place or excerpt within the model topology (e.g. the n-th track in the fork)
** - it has a temporal extension within a larger temporal frame (e.g. some seconds within
** the timeline)
** 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.
** interval, embedded into a larger time span covering a complete timeline.
** @note as of 10/2022 this component is in an early stage of development and just used
** to coordinate the horizontal extension of the timeline view.
**
** # Rationale
**
@ -98,6 +101,10 @@ namespace model {
using lib::time::FSecs;
using lib::time::Time;
namespace {
/** the deepest zoom is to use 2px per micro-tick */
const uint ZOOM_MAX_RESOLUTION = 2 * TimeValue::SCALE;
}
/**
* A component to ensure uniform handling of zoom scale
@ -105,6 +112,13 @@ namespace model {
* the mutator functions are validated and harmonised to
* meet the internal invariants; a change listener is
* possibly notified to pick up the new settings.
*
* A ZoomWindow...
* - is a #visible TimeSpan
* - which is completely inside an #overalSpan
* - and is rendered at a scale factor #px_per_sec
* - 0 < px_per_sec <= ZOOM_MAX_RESOLUTION
* - zoom operations are applied around an #anchorPoint
*/
class ZoomWindow
: util::NonCopyable
@ -152,7 +166,12 @@ namespace model {
void
nudgeMetric (int steps)
{
UNIMPLEMENTED ("nudgeMetric");
uint changedScale =
steps > 0 ? px_per_sec_ << steps
: px_per_sec_ >> -steps;
if (0 < changedScale
and changedScale <= ZOOM_MAX_RESOLUTION)
mutateScale (changedScale);
}
void
@ -239,7 +258,39 @@ namespace model {
void
mutateScale (uint px_per_sec)
{
UNIMPLEMENTED ("change scale factor, validate and adjust all params");
if (px_per_sec == 0) px_per_sec = 1;
if (px_per_sec == px_per_sec_) return;
FSecs changeFactor{px_per_sec, px_per_sec_};
FSecs dur{afterWin_ - startWin_};
dur /= changeFactor;
if (dur > FSecs{afterAll_ - startAll_})
{// limit to the overall timespan...
px_per_sec_ = adjustedScale (startAll_,afterAll_, startWin_,afterWin_);
startWin_ = startAll_;
afterWin_ = afterAll_;
}
else
{
TimeVar start{anchorPoint() - dur*relativeAnchor()};
if (start < startAll_)
start = startAll_;
TimeVar after{start + dur};
if (after > afterAll_)
{
after = afterAll_;
start = afterAll_ - dur;
}
ASSERT (after-start <= afterAll_-startAll_);
if (start == startWin_ and after == afterWin_)
return; // nothing changed effectively
px_per_sec_ = adjustedScale (start,after, startWin_,afterWin_);
startWin_ = start;
afterWin_ = after;
}
fireChangeNotification();
}
void
@ -247,6 +298,68 @@ namespace model {
{
UNIMPLEMENTED ("change visible duration, validate and adjust all params");
}
/**
* Adjust the display scale such as to match the given changed time interval
* @param startNew changed start point
* @param afterNew changed end point
* @param startOld previous start point
* @param afterOld previous end point
* @return adapted scale factor in pixel per second, rounded half up to the next pixel.
*/
uint
adjustedScale (TimeVar startNew, TimeVar afterNew, TimeVar startOld, TimeVar afterOld)
{
REQUIRE (startOld < afterOld);
FSecs factor = FSecs{afterNew - startNew} / FSecs{afterOld - startOld};
return boost::rational_cast<uint>(px_per_sec_ / factor + 1/2); // rounding half pixels
}
/**
* The anchor point or centre for zooming operations applied to the visible window
* @return where the visible window should currently be anchored
* @remark this point can sometimes be outside the current visible window,
* but any further zooming/scaling/scrolling operation should bring it back
* into sight. Moreover, the function #relativeAnchor() defines the position
* where this anchor point _should_ be placed relative to the visible window.
* @todo 10/2022 we use a numerical rule currently, but that could be contextual state,
* like e.g. the current position of the play head or edit cursor or mouse.
*/
FSecs
anchorPoint() const
{
return startWin_ + FSecs{afterWin_-startWin_} * relativeAnchor();
}
/**
* define at which proportion to the visible window's duration the anchor should be placed
* @return a fraction 0 ... 1, where 0 means at start and 1 means after end.
* @note as of 10/2022 we use a numerical rule to place the anchor point in accordance
* to the current visible window's position within the overall timeline; if it's
* close to the beginning, the anchor point is also rather to the beginning...
*/
FSecs
relativeAnchor() const
{
// the visible window itself has to fit in, which reduces the action range
FSecs possibleRange = (afterAll_-startAll_) - (afterWin_-startWin_);
if (possibleRange == 0) // if there is no room for scrolling...
return FSecs{1,2}; // then anchor zooming in the middle
// use a 3rd degree parabola to favour positions in the middle
FSecs posFactor = FSecs{startWin_-startAll_} / possibleRange;
posFactor = (2*posFactor - 1); // -1 ... +1
posFactor = posFactor*posFactor*posFactor; // -1 ... +1 but accelerating towards boundraries
posFactor = (posFactor + 1) / 2; // 0 ... 1
return posFactor;
}
void
fireChangeNotification()
{
TODO("really fire...");
}
};

View file

@ -89,7 +89,7 @@ namespace test {
zoomWin.nudgeMetric(+1);
CHECK (zoomWin.px_per_sec() == 50);
CHECK (zoomWin.visible() == TimeSpan(Time(FSecs(23,4)), Time(FSecs(23,2))));
CHECK (zoomWin.visible() == TimeSpan(Time(FSecs(23,4)), FSecs(23,2)));
CHECK (zoomWin.overallSpan() == TimeSpan(Time::ZERO, Time(FSecs(23))));
}

View file

@ -38379,8 +38379,14 @@
</html></richcontent>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1666966669487" ID="ID_1477573565" MODIFIED="1666967820569" TEXT="testgetrieben entwickelt">
<icon BUILTIN="flag-yellow"/>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1666966669487" ID="ID_1477573565" MODIFIED="1667093086439" TEXT="testgetrieben entwickelt">
<icon BUILTIN="pencil"/>
<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"/>
<node CREATED="1667093120846" ID="ID_1405458968" MODIFIED="1667093134771" TEXT="&#x27f9; doppelte Aufl&#xf6;sung"/>
<node CREATED="1667093136007" ID="ID_1309995367" MODIFIED="1667093153233" TEXT="&#x27f9; visible Window liegt in der Mitte und hat hable L&#xe4;nge"/>
</node>
</node>
</node>
<node BACKGROUND_COLOR="#d2beaf" COLOR="#5c4d6e" CREATED="1666913274942" ID="ID_4743528" MODIFIED="1666913286194" TEXT="Design &#xfc;berpr&#xfc;fen">