TimelineElision.h header

#include <ew/app/TimelineElision.h>

Namespace ew::app

TimelineElision class

class ew::app::TimelineElision

A piecewise-linear map between real story time and a compressed "layout" axis, so a long empty span (a 3,000-year backstory before an eleven-day plot) becomes a thin break instead of pushing the plot a millionth of the way across the screen (docs/TIMELINE_UX.md TL-23, docs/ TIMELINE_AUDIT.md TA-4).

Why a separate transform and not a change to TimelineScale. The scale must stay AFFINE: zoom-at-anchor and pan and fit all depend on a single time-per-pixel slope and an invertible map. Elision is inherently PIECEWISE, so it sits IN FRONT of the scale – the panel composes real instant -> layoutForInstant -> [affine TimelineScale] -> pixel. The scale then zooms and pans the layout axis, none the wiser that some of it is compressed, and stays exactly as testable as before.

The map is built from the occupied intervals (the union of the events' spans): each occupied span and each gap no longer than gapThreshold is kept 1:1; each gap longer than the threshold collapses to a fixed collapsedLayout width. Layout is anchored so the first occupied instant is its own layout value, so an all-kept axis is the identity and nothing moves when there is nothing to elide.

Members

void ew::app::TimelineElision::build(std::vector< TimelineInterval > occupied, qint64 gapThreshold, qint64 collapsedLayout, const std::vector< qint64 > &keptGapStarts={})

Builds the map. occupied need not be sorted or disjoint – it is merged first. A gap whose real length exceeds gapThreshold collapses to collapsedLayout layout-minutes; both are clamped to at least zero / one respectively so the map is always well-formed. With no occupied intervals the map is empty and acts as the identity.

A gap whose real start instant is in keptGapStarts is NEVER collapsed, however long it is: that is how "click to expand" works – the panel remembers which breaks the user opened and keeps them at full scale so the empty span is navigable, while every other void stays thin.

qint64 ew::app::TimelineElision::layoutForInstant(qint64 instant) const

Real instant -> layout minute. Monotone non-decreasing, so it composes with the affine scale. Outside the mapped span it extends at slope 1 from the nearest end (the axis is unbounded, so an instant before the first / after the last occupied span still has a place).

qint64 ew::app::TimelineElision::instantForLayout(qint64 layout) const

Layout minute -> real instant, the inverse. Exact on kept pieces; on a collapsed gap it is necessarily lossy (many real minutes share few layout minutes) but stays monotone and lands inside the gap, which is all a pan or a click on the break needs.

const std::vector< TimelineSegment > & ew::app::TimelineElision::segments() const

The segments, in order. The panel walks these to draw a break marker for every TimelineSegment::collapsed piece (its real span gives the "N years" label; its layout span gives where to draw it) and to expand one on click.

bool ew::app::TimelineElision::hasElision() const

Whether any gap was collapsed – i.e. whether there is anything to draw a break for. When false the map is the identity and the panel can skip the elision path entirely.

TimelineInterval struct

struct ew::app::TimelineInterval

A stretch of story time that is occupied by content – the merged span of one or more events. Elision keeps these at full scale and collapses only the empty gaps between them.

Members

qint64 ew::app::TimelineInterval::start = 0

Start instant, in minutes since the story epoch.

qint64 ew::app::TimelineInterval::end = 0

Never before start; an instant is start == end.

TimelineSegment struct

struct ew::app::TimelineSegment

One piece of the piecewise-linear elision map: real minutes [realStart, realEnd] map affinely onto layout minutes [layoutStart, layoutEnd]. A kept piece has equal real and layout length (slope 1, exactly invertible); a collapsed gap has a fixed small layout length over whatever real span it hides, which is what draws as a thin break marker (TL-23).

Members

qint64 ew::app::TimelineSegment::realStart = 0

Real-time start of this piece (minutes since the story epoch).

qint64 ew::app::TimelineSegment::realEnd = 0

Real-time end of this piece.

qint64 ew::app::TimelineSegment::layoutStart = 0

The layout-axis start this piece maps onto.

qint64 ew::app::TimelineSegment::layoutEnd = 0

The layout-axis end this piece maps onto.

bool ew::app::TimelineSegment::collapsed = false

Whether this piece is a collapsed empty gap (drawn as a thin break marker).

bool ew::app::TimelineSegment::keptByUser = false

True only for a gap that WOULD collapse (its real length exceeds the threshold) but is kept at full scale because the user opened it (its start is in keptGapStarts). This is what a re-collapse affordance hangs off: a naturally-small kept gap (under threshold) is not marked, so it is never offered for collapse, and neither is an occupied span.