| 123 | } |
| 124 | |
| 125 | std::optional<DisplaySeconds> AppSession::recomputeRange() { |
| 126 | // While actively following a live stream (streaming + playing), scope the range to |
| 127 | // that dataset's tip ONLY — exactly as the live-ingest path does. Unioning in |
| 128 | // far-away file datasets would push range_max past the live edge; the cursor is |
| 129 | // pinned to range_max (onTick), so that would jolt the handle/needle off the tip and |
| 130 | // back (the jitter). When paused, fall through to the full union so the user can |
| 131 | // scrub/align against ALL loaded data. |
| 132 | if (active_streaming_dataset_id_ != 0 && playback_engine_->isPlaying()) { |
| 133 | if (const auto range = session_manager_->datasetDisplayRange(active_streaming_dataset_id_); range.has_value()) { |
| 134 | playback_engine_->setRange(*range); |
| 135 | return range->min; |
| 136 | } |
| 137 | } |
| 138 | // Union the bounds in DISPLAY-relative seconds, converting each item with |
| 139 | // its dataset's OWN offset, so the playback axis matches what the plots |
| 140 | // render (display_time = raw_time - offset) rather than the absolute epoch. |
| 141 | // Range only — never currentTime, never playback_seeded_ — for the live |
| 142 | // Timeline drag path. setRange re-clamps the playhead; an in-range scrub |
| 143 | // position is preserved. |
| 144 | std::optional<DisplaySeconds> new_min; |
| 145 | std::optional<DisplaySeconds> new_max; |
| 146 | forEachVisibleRawRange([&](DatasetId dataset_id, Timestamp raw_min, Timestamp raw_max) { |
| 147 | const DisplayOffset offset = session_manager_->displayOffset(dataset_id); |
| 148 | const DisplaySeconds ds_min = rawToDisplaySeconds(raw_min, offset); |
| 149 | const DisplaySeconds ds_max = rawToDisplaySeconds(raw_max, offset); |
| 150 | new_min = new_min ? std::min(*new_min, ds_min) : ds_min; |
| 151 | new_max = new_max ? std::max(*new_max, ds_max) : ds_max; |
| 152 | }); |
| 153 | if (!new_min) { |
| 154 | // No visible data: keep the current range (nothing better to show). |
| 155 | return std::nullopt; |
| 156 | } |
| 157 | playback_engine_->setRange(DisplayRange{*new_min, *new_max}); |
| 158 | return new_min; |
| 159 | } |
| 160 | |
| 161 | std::optional<AppSession::MergePlan> AppSession::planMerge(const std::vector<DatasetId>& selected) const { |
| 162 | if (selected.size() < 2) { |