From 7145d0d9ce1ec1f7d522131ecbbbb3548e69f32c Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sun, 30 Oct 2022 01:31:25 +0200 Subject: [PATCH] Timeline: `ZoomWindow` implementation draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/time.h | 6 +- src/lib/time/time.cpp | 20 +-- src/lib/time/timevalue.hpp | 184 +++++++++++++++---------- src/stage/model/zoom-window.hpp | 125 ++++++++++++++++- tests/stage/model/zoom-window-test.cpp | 2 +- wiki/thinkPad.ichthyo.mm | 10 +- 6 files changed, 255 insertions(+), 92 deletions(-) diff --git a/src/lib/time.h b/src/lib/time.h index 2828c4c80..a6fe2aa43 100644 --- a/src/lib/time.h +++ b/src/lib/time.h @@ -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); diff --git a/src/lib/time/time.cpp b/src/lib/time/time.cpp index ce05c2cb6..7e66d1600 100644 --- a/src/lib/time/time.cpp +++ b/src/lib/time/time.cpp @@ -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::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 } diff --git a/src/lib/time/timevalue.hpp b/src/lib/time/timevalue.hpp index b435992dd..942f8aec5 100644 --- a/src/lib/time/timevalue.hpp +++ b/src/lib/time/timevalue.hpp @@ -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 frame aligned 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 - > > - { - 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 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 + > > + { + 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) { diff --git a/src/stage/model/zoom-window.hpp b/src/stage/model/zoom-window.hpp index 5b1846916..8d779b9b2 100644 --- a/src/stage/model/zoom-window.hpp +++ b/src/stage/model/zoom-window.hpp @@ -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(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..."); + } }; diff --git a/tests/stage/model/zoom-window-test.cpp b/tests/stage/model/zoom-window-test.cpp index f16c85983..83d8345f8 100644 --- a/tests/stage/model/zoom-window-test.cpp +++ b/tests/stage/model/zoom-window-test.cpp @@ -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)))); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 519dcf5d7..b59c06ae7 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -38379,8 +38379,14 @@ - - + + + + + + + +