From dd6b26bbba677c95b80b95235378cb5d381bee43 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 8 Oct 2026 13:54:23 -0700 Subject: [PATCH] jp: zoom improvements (cherry picked from commit f804d3468e05820f11e3580d6aa797bc958c7b82) --- openpilot/tools/jotpluggler/app.h | 11 ++++++ openpilot/tools/jotpluggler/plot.cc | 55 +++++++++++++++++++++++++- openpilot/tools/jotpluggler/render.cc | 12 ++++++ openpilot/tools/jotpluggler/session.cc | 3 ++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/openpilot/tools/jotpluggler/app.h b/openpilot/tools/jotpluggler/app.h index 4211a754d0..dd6c81eb23 100644 --- a/openpilot/tools/jotpluggler/app.h +++ b/openpilot/tools/jotpluggler/app.h @@ -663,6 +663,11 @@ struct UndoStack { } }; +struct PlotView { + ImGuiID plot_id = 0; + PlotRange range; +}; + struct UiState { bool open_open_route = false; bool open_stream = false; @@ -734,6 +739,12 @@ struct UiState { bool init_data_selected = false; std::string init_data_search; UndoStack undo; + std::vector plot_view_history; + std::optional restore_plot_view; + ImGuiID last_plot_view_id = 0; + double last_plot_view_change = -1.0; + bool last_plot_view_wheel = false; + double last_plot_pan_click = -1.0; }; // app.cc public API diff --git a/openpilot/tools/jotpluggler/plot.cc b/openpilot/tools/jotpluggler/plot.cc index b8697c6872..59d08ff2c9 100644 --- a/openpilot/tools/jotpluggler/plot.cc +++ b/openpilot/tools/jotpluggler/plot.cc @@ -1,6 +1,7 @@ #include "tools/jotpluggler/internal.h" #include "implot.h" +#include "implot_internal.h" #include "imgui_internal.h" #include @@ -801,7 +802,13 @@ void draw_plot(const AppSession &session, Pane *pane, UiState *state) { plot_flags |= ImPlotFlags_NoLegend; } - const ImPlotAxisFlags x_axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight; + ImPlotAxisFlags x_axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight; + const ImGuiIO &input = ImGui::GetIO(); + const bool at_zoom_limit = state->x_view_max - state->x_view_min <= MIN_HORIZONTAL_ZOOM_SECONDS + 1.0e-6; + if (at_zoom_limit && (input.MouseWheel > 0.0f || input.MouseReleased[ImGuiMouseButton_Left])) { + // Further zoom-in must not move the time window; panning and zoom-out remain available. + x_axis_flags |= ImPlotAxisFlags_LockMin | ImPlotAxisFlags_LockMax; + } ImPlotAxisFlags y_axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight; if (state_block_mode) { y_axis_flags |= ImPlotAxisFlags_NoDecorations; @@ -817,6 +824,8 @@ void draw_plot(const AppSession &session, Pane *pane, UiState *state) { if (ImPlot::BeginPlot("##plot", plot_size, plot_flags)) { ImPlot::SetupAxes(nullptr, nullptr, x_axis_flags, y_axis_flags); ImPlot::SetupAxisFormat(ImAxis_X1, "%.1f"); + // ImPlot updates pan endpoints separately; a minimum span would distort the pan. + ImPlot::SetupAxisZoomConstraints(ImAxis_X1, input.MouseDown[ImGuiMouseButton_Right] ? 0.0 : MIN_HORIZONTAL_ZOOM_SECONDS, INFINITY); if (state_block_mode) { ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, 1.0, ImPlotCond_Always); } else if (pane_value_format.valid) { @@ -841,6 +850,28 @@ void draw_plot(const AppSession &session, Pane *pane, UiState *state) { ImPlot::SetupLegend(ImPlotLocation_NorthEast); } + ImPlotPlot *plot = ImPlot::GetCurrentPlot(); + const PlotRange previous_view = {.left = previous_x_min, .right = previous_x_max, + .bottom = plot->Axes[ImAxis_Y1].Range.Min, .top = plot->Axes[ImAxis_Y1].Range.Max}; + const bool was_selecting = plot->Selecting; + const bool restoring_view = state->restore_plot_view && state->restore_plot_view->plot_id == plot->ID; + if (restoring_view) { + ImPlot::SetupAxisLimits(ImAxis_Y1, state->restore_plot_view->range.bottom, state->restore_plot_view->range.top, ImPlotCond_Always); + state->restore_plot_view.reset(); + } + const ImGuiIO &io = ImGui::GetIO(); + const ImVec2 drag(io.MousePos.x - plot->SelectStart.x, io.MousePos.y - plot->SelectStart.y); + const bool zoom_ready = ImGui::GetTime() - io.MouseClickedTime[ImGuiMouseButton_Left] >= 0.09 + && (std::abs(drag.x) > 12.0f || std::abs(drag.y) > 12.0f); + // Cancel before ImPlot processes release; hide the box until both thresholds are met. + if (io.MouseReleased[ImGuiMouseButton_Left] && !zoom_ready) { + plot->Selecting = false; + } + ImPlot::SetupFinish(); + if (!zoom_ready) { + plot->Selected = false; + } + if (state_block_mode) { draw_state_blocks_pane(prepared_curves, state); } else { @@ -873,6 +904,28 @@ void draw_plot(const AppSession &session, Pane *pane, UiState *state) { state->tracker_time = std::clamp(ImPlot::GetPlotMousePos().x, state->route_x_min, state->route_x_max); state->has_tracker_time = true; } + const ImPlotRect view = ImPlot::GetPlotLimits(); + const bool changed = std::abs(view.X.Min - previous_view.left) > 1.0e-6 + || std::abs(view.X.Max - previous_view.right) > 1.0e-6 + || std::abs(view.Y.Min - previous_view.bottom) > 1.0e-6 + || std::abs(view.Y.Max - previous_view.top) > 1.0e-6; + const bool wheel = io.MouseWheel != 0.0f; + const bool pan = ImGui::IsMouseDown(ImGuiMouseButton_Right); + if (changed && (ImPlot::IsPlotHovered() || was_selecting || plot->Held) && (wheel || pan || io.MouseReleased[ImGuiMouseButton_Left]) + && !restoring_view && !state->suppress_range_side_effects && !plot->JustCreated) { + // Keep continuous pans and wheel bursts as one view-history entry. + const bool continuing = state->last_plot_view_id == plot->ID + && ((pan && state->last_plot_pan_click == io.MouseClickedTime[ImGuiMouseButton_Right]) + || (wheel && state->last_plot_view_wheel && ImGui::GetTime() - state->last_plot_view_change < 0.3)); + if (!continuing) { + if (state->plot_view_history.size() == 50) state->plot_view_history.erase(state->plot_view_history.begin()); + state->plot_view_history.push_back({plot->ID, previous_view}); + } + state->last_plot_view_id = plot->ID; + state->last_plot_view_change = ImGui::GetTime(); + state->last_plot_view_wheel = wheel; + state->last_plot_pan_click = io.MouseClickedTime[ImGuiMouseButton_Right]; + } ImPlot::EndPlot(); } app_pop_mono_font(); diff --git a/openpilot/tools/jotpluggler/render.cc b/openpilot/tools/jotpluggler/render.cc index f7d991d2f9..87f88c955d 100644 --- a/openpilot/tools/jotpluggler/render.cc +++ b/openpilot/tools/jotpluggler/render.cc @@ -68,6 +68,18 @@ void render_layout(AppSession *session, UiState *state, bool show_camera_feed) { if (!ImGui::GetIO().WantTextInput && ImGui::IsKeyPressed(ImGuiKey_Space, false)) { state->playback_playing = !state->playback_playing; } + // GLFW/ImGui mouse button 4 is the conventional mouse Back button. + if (ImGui::IsMouseClicked(3) && !state->plot_view_history.empty()) { + const PlotView view = state->plot_view_history.back(); + state->plot_view_history.pop_back(); + state->x_view_min = view.range.left; + state->x_view_max = view.range.right; + state->restore_plot_view = view; + state->follow_latest = false; + state->playback_playing = false; + state->suppress_range_side_effects = true; + state->last_plot_view_id = 0; + } advance_playback(state, *session); CameraFeedView *sidebar_camera = session->pane_camera_feeds[static_cast(sidebar_preview_camera_view(*session))].get(); if (show_camera_feed && sidebar_camera != nullptr && state->has_tracker_time) { diff --git a/openpilot/tools/jotpluggler/session.cc b/openpilot/tools/jotpluggler/session.cc index 71a62fc7e6..fbadcc03f1 100644 --- a/openpilot/tools/jotpluggler/session.cc +++ b/openpilot/tools/jotpluggler/session.cc @@ -31,6 +31,9 @@ void apply_route_data(AppSession *session, UiState *state, RouteData route_data) } else if (session->route_name.empty() && session->data_mode == SessionDataMode::Route) { session->route_id = {}; } + state->plot_view_history.clear(); + state->last_plot_view_id = 0; + state->restore_plot_view.reset(); session->route_data = std::move(route_data); rebuild_route_index(session); rebuild_browser_nodes(session, state);