From ce3fc0ccfb00f84d5f3facb0b0648dd5abc3af0c Mon Sep 17 00:00:00 2001 From: Saran Tunyasuvunakool Date: Wed, 8 Jul 2026 13:34:28 -0700 Subject: [PATCH] Replace history slider with a custom timeline scrubber. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new timeline widget in the Studio UI, replacing the previous integer slider for simulation history. The new widget displays the current and maximum simulation times, formatted with appropriate units (µs, ms, s), alongside the actual step number, and allows scrubbing through the history via a custom-drawn spine and knob. This change also modifies the history recording logic to record every solver step rather than only every rendered frame. This enables the step forward/backward buttons to navigate exactly 1 solver step at a time. This change also fixes `SimHistory::SetIndex()` range checks in `sim_history.cc` to clamp `offset_` against `1 - size_` (`Size()`) rather than total vector capacity (`history_.size()`). PiperOrigin-RevId: 944671699 Change-Id: Iaa34668a1e257e5c0c1840c827370d81c4935b3d --- src/experimental/platform/sim/sim_history.cc | 9 +- src/experimental/studio/app.cc | 424 ++++++++++++++++--- src/experimental/studio/app.h | 7 + 3 files changed, 375 insertions(+), 65 deletions(-) diff --git a/src/experimental/platform/sim/sim_history.cc b/src/experimental/platform/sim/sim_history.cc index b32d7232..a8a63fd1 100644 --- a/src/experimental/platform/sim/sim_history.cc +++ b/src/experimental/platform/sim/sim_history.cc @@ -61,15 +61,12 @@ std::span SimHistory::AddToHistory() { } std::span SimHistory::SetIndex(int offset) { - const int size = history_.size(); - if (size > 0) { - offset_ = std::clamp(offset, 1 - size, 0); - } - - if (history_.empty()) { + if (history_.empty() || size_ <= 0) { return {}; } + offset_ = std::clamp(offset, 1 - size_, 0); + const int actual_index = (cursor_ - 1 + offset_ + history_.size()) % history_.size(); return history_[actual_index]; diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index f95966bf..55f7a1d5 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -106,9 +107,7 @@ App::App(Config config) [this](const mjModel* m, mjData* d) { PostStep(m, d); }); } -App::~App() { - mjv_freeScene(&plugin_scene_); -} +App::~App() { mjv_freeScene(&plugin_scene_); } void App::SwitchGraphicsMode(int width, int height, platform::GraphicsMode mode) { @@ -135,6 +134,17 @@ void App::Recompile() { model_holder_->model(), model_holder_->data()); const int state_size = mj_stateSize(model(), mjSTATE_INTEGRATION); sim_history_.Init(state_size); + if (has_model() && has_data()) { + std::span state = sim_history_.AddToHistory(); + if (!state.empty()) { + mj_getState(model(), data(), state.data(), mjSTATE_INTEGRATION); + } + } + tmp_.sim_head_time_ = has_data() ? data()->time : 0.0; + tmp_.timeline_lh_width = 0.0f; + tmp_.timeline_rh_width = 0.0f; + tmp_.scrubber_active = false; + tmp_.scrubber_grab_offset = 0.0f; } void App::RequestModelLoad(std::string model_file) { @@ -222,6 +232,17 @@ void App::OnModelLoaded(std::string filename, ModelKind model_kind) { renderer_->Init(model); const int state_size = mj_stateSize(model, mjSTATE_INTEGRATION); sim_history_.Init(state_size); + if (has_model() && has_data()) { + std::span state = sim_history_.AddToHistory(); + if (!state.empty()) { + mj_getState(model, data(), state.data(), mjSTATE_INTEGRATION); + } + } + tmp_.sim_head_time_ = has_data() ? data()->time : 0.0; + tmp_.timeline_lh_width = 0.0f; + tmp_.timeline_rh_width = 0.0f; + tmp_.scrubber_active = false; + tmp_.scrubber_grab_offset = 0.0f; if (!preserve_camera_on_load_) { const int model_cam = model->vis.global.cameraid; @@ -284,6 +305,21 @@ void App::SetLoadError(std::string error) { void App::ResetPhysics() { mj_resetData(model(), data()); mj_forward(model(), data()); + if (has_model()) { + const int state_size = mj_stateSize(model(), mjSTATE_INTEGRATION); + sim_history_.Init(state_size); + } + if (has_model() && has_data()) { + std::span state = sim_history_.AddToHistory(); + if (!state.empty()) { + mj_getState(model(), data(), state.data(), mjSTATE_INTEGRATION); + } + } + tmp_.sim_head_time_ = has_data() ? data()->time : 0.0; + tmp_.timeline_lh_width = 0.0f; + tmp_.timeline_rh_width = 0.0f; + tmp_.scrubber_active = false; + tmp_.scrubber_grab_offset = 0.0f; step_error_ = ""; edit_error_ = ""; } @@ -298,16 +334,17 @@ void App::UpdatePhysics() { tmp_.update_threadpool = false; } - bool stepped = false; + bool plugin_stepped = false; platform::ForEachPlugin([&](auto* plugin) { if (plugin->do_update) { if (plugin->do_update(plugin, model(), data())) { - stepped = true; + plugin_stepped = true; } } }); - if (!stepped) { + bool stepped = plugin_stepped; + if (!plugin_stepped) { if (step_control_.GetPauseState() != PauseState::kNormalPaused) { mju_zero(data()->xfrc_applied, 6 * model()->nbody); mjv_applyPerturbPose(model(), data(), &perturb_, 0); @@ -345,9 +382,12 @@ void App::UpdatePhysics() { if (stepped) { profiler_.Update(model(), data()); - std::span state = sim_history_.AddToHistory(); - if (!state.empty()) { - mj_getState(model(), data(), state.data(), mjSTATE_INTEGRATION); + if (plugin_stepped) { + std::span state = sim_history_.AddToHistory(); + if (!state.empty()) { + mj_getState(model(), data(), state.data(), mjSTATE_INTEGRATION); + tmp_.sim_head_time_ = data()->time; + } } } } @@ -366,6 +406,12 @@ void App::PostStep(const mjModel* m, mjData* d) { plugin->post_step(plugin, m, d); } }); + + std::span state = sim_history_.AddToHistory(); + if (!state.empty()) { + mj_getState(m, d, state.data(), mjSTATE_INTEGRATION); + tmp_.sim_head_time_ = d->time; + } } void App::LoadHistory(int offset) { @@ -921,7 +967,8 @@ void App::BuildGui() { ImGui::GetIO().FontGlobalScale = ui_.font_scale; - const ImVec4 workspace_rect = platform::ConfigureDockingLayout(tmp_.toolbar, tmp_.status_bar); + const ImVec4 workspace_rect = + platform::ConfigureDockingLayout(tmp_.toolbar, tmp_.status_bar); // Place charts in bottom right corner of the workspace. const ImVec2 chart_size(250, 500); @@ -976,8 +1023,10 @@ void App::BuildGui() { if (tmp_.profiler) { if (ImGui::Begin("Profiler", &tmp_.profiler, - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { - platform::ProfilerGui(model(), data(), &profiler_, tmp_.profiler_show_iter); + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse)) { + platform::ProfilerGui(model(), data(), &profiler_, + tmp_.profiler_show_iter); } ImGui::End(); } @@ -996,8 +1045,7 @@ void App::BuildGui() { ImGui::SetNextWindowPos(ImVec2(workspace_rect.x, workspace_rect.y), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_Appearing); - if (ImGui::Begin("Help", &tmp_.help, - ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::Begin("Help", &tmp_.help, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 5.0f)); HelpGui(); ImGui::PopStyleVar(); @@ -1007,13 +1055,13 @@ void App::BuildGui() { if (!load_error_.empty()) { const float scale = ImGui::GetWindowDpiScale(); - const float max_line_width = - ImGui::CalcTextSize(load_error_.c_str()).x; + const float max_line_width = ImGui::CalcTextSize(load_error_.c_str()).x; const float padding = 30.0f * scale; const float max_workspace_width = std::max(0.0f, workspace_rect.z - 20.0f); - const float min_target_width = std::min(350.0f * scale, max_workspace_width); - const float target_width = std::clamp(max_line_width + padding, - min_target_width, max_workspace_width); + const float min_target_width = + std::min(350.0f * scale, max_workspace_width); + const float target_width = std::clamp( + max_line_width + padding, min_target_width, max_workspace_width); if (platform::BeginOverlay("WarningOverlay", platform::OverlayPos::kBottom, workspace_rect, target_width, 0.8f)) { @@ -1054,28 +1102,25 @@ void App::BuildGui() { // pause overlay if (tmp_.info && has_model() && step_control_.GetPauseState() == PauseState::kNormalPaused) { - platform::TextOverlay("Pause", platform::OverlayPos::kTop, - workspace_rect, "PAUSE", ImVec4(0, 0, 0, 0), 3.0f); + platform::TextOverlay("Pause", platform::OverlayPos::kTop, workspace_rect, + "PAUSE", ImVec4(0, 0, 0, 0), 3.0f); } // realtime factor overlay - if (has_model() && - step_control_.GetPauseState() == PauseState::kUnpaused) { + if (has_model() && step_control_.GetPauseState() == PauseState::kUnpaused) { const float desired = step_control_.GetSpeed(); const float measured = step_control_.GetSpeedMeasured(); - if (desired != 100.0f || - mju_abs(measured - desired) > 0.1f * desired) { + if (desired != 100.0f || mju_abs(measured - desired) > 0.1f * desired) { char rtlabel[30]; bool misaligned = mju_abs(measured - desired) > 0.1f * desired; if (misaligned) { - std::snprintf(rtlabel, sizeof(rtlabel), "%g%% (%-4.1f%%)", - desired, measured); + std::snprintf(rtlabel, sizeof(rtlabel), "%g%% (%-4.1f%%)", desired, + measured); } else { std::snprintf(rtlabel, sizeof(rtlabel), "%g%%", desired); } platform::TextOverlay("Realtime", platform::OverlayPos::kTopLeft, - workspace_rect, rtlabel, ImVec4(0, 0, 0, 0), - 2.0f); + workspace_rect, rtlabel, ImVec4(0, 0, 0, 0), 2.0f); } } @@ -1150,6 +1195,44 @@ void App::BuildGui() { } } +static std::string FormatTimelineTime(double time_in_s) { + if (time_in_s < 0.0) time_in_s = 0.0; + char buf[64]; + if (time_in_s == 0.0) { + return "0.00s"; + } else if (time_in_s < 1e-3) { + // Microseconds range. + const double t_us = time_in_s * 1e6; + if (t_us >= 100.0) { + std::snprintf(buf, sizeof(buf), "%.0f\xC2\xB5s", t_us); + } else if (t_us >= 10.0) { + std::snprintf(buf, sizeof(buf), "%.1f\xC2\xB5s", t_us); + } else { + std::snprintf(buf, sizeof(buf), "%.2f\xC2\xB5s", t_us); + } + } else if (time_in_s < 1.0) { + // Milliseconds range. + const double t_ms = time_in_s * 1e3; + if (t_ms >= 100.0) { + std::snprintf(buf, sizeof(buf), "%.0fms", t_ms); + } else if (t_ms >= 10.0) { + std::snprintf(buf, sizeof(buf), "%.1fms", t_ms); + } else { + std::snprintf(buf, sizeof(buf), "%.2fms", t_ms); + } + } else { + // Seconds range. + if (time_in_s >= 100.0) { + std::snprintf(buf, sizeof(buf), "%.0fs", time_in_s); + } else if (time_in_s >= 10.0) { + std::snprintf(buf, sizeof(buf), "%.1fs", time_in_s); + } else { + std::snprintf(buf, sizeof(buf), "%.2fs", time_in_s); + } + } + return buf; +} + void App::ModelOptionsGui() { const float min_width = platform::GetExpectedLabelWidth(); const ImGuiChildFlags child_flags = @@ -1241,16 +1324,15 @@ void App::ModelOptionsGui() { { const int max_idx = platform::kPercentRealTime.size() - 1; int slider_val = max_idx - tmp_.speed_index; - float speed_pct = - std::stof(platform::kPercentRealTime[tmp_.speed_index]); + float speed_pct = std::stof(platform::kPercentRealTime[tmp_.speed_index]); char fmt[64]; const float desired = step_control_.GetSpeed(); const float measured = step_control_.GetSpeedMeasured(); bool misaligned = std::abs(measured - desired) > 0.1f * desired; if (misaligned) { - std::snprintf(fmt, sizeof(fmt), "%.1f%%%% (%.1f%%%%)", - speed_pct, measured); + std::snprintf(fmt, sizeof(fmt), "%.1f%%%% (%.1f%%%%)", speed_pct, + measured); } else { std::snprintf(fmt, sizeof(fmt), "%.1f%%%%", speed_pct); } @@ -1272,9 +1354,11 @@ void App::ModelOptionsGui() { ImGui::Separator(); ImGui::Spacing(); char prev_label[32]; - std::snprintf(prev_label, sizeof(prev_label), "%s Step Back", ICON_PREV_FRAME); + std::snprintf(prev_label, sizeof(prev_label), "%s Step Back", + ICON_PREV_FRAME); char next_label[32]; - std::snprintf(next_label, sizeof(next_label), "%s Step Fwd", ICON_NEXT_FRAME); + std::snprintf(next_label, sizeof(next_label), "%s Step Fwd", + ICON_NEXT_FRAME); const float avail = ImGui::GetContentRegionAvail().x; const float spacing = ImGui::GetStyle().ItemSpacing.x; @@ -1294,12 +1378,235 @@ void App::ModelOptionsGui() { } ImGui::SetItemTooltip("%s", "Load next frame from history / Single step"); - ImGui::SetNextItemWidth(slider_w); - int index = sim_history_.GetIndex(); - if (ImGui::SliderInt("History", &index, 1 - sim_history_.Size(), 0)) { - LoadHistory(index); + // Timeline scrubber: spine + sliding knob widget. + { + const double max_time = tmp_.sim_head_time_; + const int hist_size = sim_history_.Size(); + const int hist_min = 1 - hist_size; // most negative index (oldest) + const int hist_max = 0; // index 0 = most recent + int current_index = sim_history_.GetIndex(); + const bool locked = (hist_size <= 1); + + // Compute current timestamp for LH label. + double curr_time = + (has_data() && current_index == sim_history_.GetIndex()) + ? data()->time + : (max_time + + current_index * + (has_model() ? model()->opt.timestep : 0.002)); + if (curr_time < 0.0) curr_time = 0.0; + + const int curr_step = (has_model() && model()->opt.timestep > 0) + ? static_cast(std::round( + curr_time / model()->opt.timestep)) + : 0; + const int max_step = + (has_model() && model()->opt.timestep > 0) + ? static_cast(std::round(max_time / model()->opt.timestep)) + : 0; + + // Both LH and RH labels aim to be at 3 digits, switching units + // independently. + std::string lh_top_str = FormatTimelineTime(curr_time); + std::string rh_top_str = FormatTimelineTime(max_time); + std::string lh_bot_str = "(" + std::to_string(curr_step) + ")"; + std::string rh_bot_str = "(" + std::to_string(max_step) + ")"; + const char* lh_top_label = lh_top_str.c_str(); + const char* rh_top_label = rh_top_str.c_str(); + const char* lh_bot_label = lh_bot_str.c_str(); + const char* rh_bot_label = rh_bot_str.c_str(); + + // Use slightly smaller text for timeline labels (e.g. 90% scale). + ImGui::SetWindowFontScale(0.9f); + + const ImVec2 cursor = ImGui::GetCursorScreenPos(); + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float base_label_w = + std::max(ImGui::CalcTextSize("88.8 ms").x, + ImGui::CalcTextSize("88.8 \xC2\xB5s").x); + const float curr_lh_w = + std::max({base_label_w, ImGui::CalcTextSize(lh_top_label).x, + ImGui::CalcTextSize(lh_bot_label).x}); + if (curr_lh_w > tmp_.timeline_lh_width) { + tmp_.timeline_lh_width = curr_lh_w; + } + const float curr_rh_w = + std::max({base_label_w, ImGui::CalcTextSize(rh_top_label).x, + ImGui::CalcTextSize(rh_bot_label).x}); + if (curr_rh_w > tmp_.timeline_rh_width) { + tmp_.timeline_rh_width = curr_rh_w; + } + const float lh_box_w = tmp_.timeline_lh_width; + const float rh_box_w = tmp_.timeline_rh_width; + const float track_w = + std::max(10.0f, ImGui::GetContentRegionAvail().x - lh_box_w - + rh_box_w - spacing * 2.0f); + + const float spine_h = 3.0f; + const float knob_w = 12.0f; + const float knob_h = 27.0f; // 1.5x taller than 18.0f + const float text_line_h = ImGui::GetTextLineHeight(); + const float custom_line_spacing = + 1.0f; // Decreased spacing between the two lines + const float text_2lines_h = text_line_h * 2.0f + custom_line_spacing; + const float total_h = std::max(knob_h, text_2lines_h); + + // Vertical center of the track row. + const float row_center_y = cursor.y + total_h * 0.5f; + const float track_x0 = cursor.x + lh_box_w + spacing; + const float spine_y0 = row_center_y - spine_h * 0.5f; + const float spine_y1 = row_center_y + spine_h * 0.5f; + const float spine_x0 = track_x0; + const float spine_x1 = track_x0 + track_w; + + // Compute knob position [0,1] along the spine. + float t = 1.0f; // Default: pinned to right end. + if (!locked) { + t = static_cast(current_index - hist_min) / + static_cast(hist_max - hist_min); + } + const float knob_cx = spine_x0 + t * (track_w - knob_w) + knob_w * 0.5f; + + // Invisible interaction button over the full track area. + ImGui::SetCursorScreenPos( + ImVec2(track_x0, row_center_y - total_h * 0.5f)); + ImGui::InvisibleButton("##TimelineScrubber", ImVec2(track_w, total_h)); + const bool hovered = ImGui::IsItemHovered(); + const bool active = ImGui::IsItemActive(); + + // Handle dragging. + if (!locked && (active || (hovered && ImGui::IsMouseDown( + ImGuiMouseButton_Left)))) { + const float mouse_x = ImGui::GetIO().MousePos.x; + if (!tmp_.scrubber_active) { + tmp_.scrubber_active = true; + if (std::abs(mouse_x - knob_cx) <= knob_w) { + tmp_.scrubber_grab_offset = mouse_x - knob_cx; + } else { + tmp_.scrubber_grab_offset = 0.0f; + } + } + + const float effective_mouse_x = mouse_x - tmp_.scrubber_grab_offset; + const float raw_t = (effective_mouse_x - spine_x0 - knob_w * 0.5f) / + std::max(1.0f, track_w - knob_w); + const float clamped_t = std::clamp(raw_t, 0.0f, 1.0f); + + int new_index = current_index; + const float snap_eps = std::max( + 0.02f, std::min(0.05f, 8.0f / std::max(1.0f, track_w - knob_w))); + if (clamped_t <= snap_eps || + effective_mouse_x <= spine_x0 + knob_w * 0.5f + 6.0f) { + new_index = hist_min; + } else if (clamped_t >= 1.0f - snap_eps || + effective_mouse_x >= spine_x1 - knob_w * 0.5f - 6.0f) { + new_index = hist_max; + } else { + const float inner_t = + (clamped_t - snap_eps) / (1.0f - 2.0f * snap_eps); + new_index = + hist_min + + static_cast(std::round(inner_t * (hist_max - hist_min))); + } + + if (new_index != current_index) { + LoadHistory(new_index); + current_index = new_index; + t = static_cast(current_index - hist_min) / + static_cast(hist_max - hist_min); + if (has_data() && current_index == sim_history_.GetIndex()) { + curr_time = data()->time; + if (curr_time < 0.0) curr_time = 0.0; + lh_top_str = FormatTimelineTime(curr_time); + lh_top_label = lh_top_str.c_str(); + const int updated_curr_step = + (has_model() && model()->opt.timestep > 0) + ? static_cast( + std::round(curr_time / model()->opt.timestep)) + : 0; + lh_bot_str = "(" + std::to_string(updated_curr_step) + ")"; + lh_bot_label = lh_bot_str.c_str(); + const float updated_lh_w = + std::max({base_label_w, ImGui::CalcTextSize(lh_top_label).x, + ImGui::CalcTextSize(lh_bot_label).x}); + if (updated_lh_w > tmp_.timeline_lh_width) { + tmp_.timeline_lh_width = updated_lh_w; + } + } + } + } else { + tmp_.scrubber_active = false; + } + + // Draw LH labels (two lines, both right-aligned). + const float text_y0 = row_center_y - text_2lines_h * 0.5f; + const float text_y1 = text_y0 + text_line_h + custom_line_spacing; + + const float lh_top_x = + cursor.x + lh_box_w - ImGui::CalcTextSize(lh_top_label).x; + ImGui::SetCursorScreenPos(ImVec2(lh_top_x, text_y0)); + ImGui::TextUnformatted(lh_top_label); + + const float lh_bot_x = + cursor.x + lh_box_w - ImGui::CalcTextSize(lh_bot_label).x; + ImGui::SetCursorScreenPos(ImVec2(lh_bot_x, text_y1)); + ImGui::TextUnformatted(lh_bot_label); + + // Draw RH labels (two lines, both left-aligned). + const float rh_x0 = track_x0 + track_w + spacing; + ImGui::SetCursorScreenPos(ImVec2(rh_x0, text_y0)); + ImGui::TextUnformatted(rh_top_label); + + ImGui::SetCursorScreenPos(ImVec2(rh_x0, text_y1)); + ImGui::TextUnformatted(rh_bot_label); + + // Restore window font scale. + ImGui::SetWindowFontScale(1.0f); + + // Draw spine and knob on the draw list. + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImGuiStyle& style = ImGui::GetStyle(); + + // Spine. + const ImVec4 scrollbar_bg = + ImGui::GetStyle().Colors[ImGuiCol_ScrollbarBg]; + ImGuiCol bg_col_idx; + if (active) { + bg_col_idx = ImGuiCol_FrameBgActive; + } else if (hovered) { + bg_col_idx = ImGuiCol_FrameBgHovered; + } else { + bg_col_idx = (scrollbar_bg.w > 0.01f) ? ImGuiCol_ScrollbarBg + : ImGuiCol_FrameBg; + } + const ImU32 spine_col = ImGui::GetColorU32(bg_col_idx); + dl->AddRectFilled(ImVec2(spine_x0, spine_y0), + ImVec2(spine_x1, spine_y1), spine_col, + spine_h * 0.5f); + + // Knob rectangle. + const float knob_x0 = knob_cx - knob_w * 0.5f; + const float knob_x1 = knob_cx + knob_w * 0.5f; + const float knob_y0 = row_center_y - knob_h * 0.5f; + const float knob_y1 = row_center_y + knob_h * 0.5f; + + ImU32 knob_col; + if (active) { + knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrabActive); + } else if (hovered) { + knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrab); + } else { + knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrab); + } + dl->AddRectFilled(ImVec2(knob_x0, knob_y0), ImVec2(knob_x1, knob_y1), + knob_col, style.GrabRounding); + // Knob border. + dl->AddRect(ImVec2(knob_x0, knob_y0), ImVec2(knob_x1, knob_y1), + ImGui::GetColorU32(ImGuiCol_Border), style.GrabRounding); + + // Advance layout cursor past this row. + ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + total_h)); } - ImGui::SetItemTooltip("%s", "Scrub through simulation history"); } // Keyframe controls. @@ -1391,9 +1698,8 @@ void App::ModelOptionsGui() { char frame_label_tmp[32]; std::snprintf(frame_label_tmp, sizeof(frame_label_tmp), " %s Camera", platform::ICON_FA_CAMERA); - const float combo_w = - -ImGui::CalcTextSize(frame_label_tmp).x - - ImGui::GetStyle().ItemInnerSpacing.x; + const float combo_w = -ImGui::CalcTextSize(frame_label_tmp).x - + ImGui::GetStyle().ItemInnerSpacing.x; // Camera selector. { @@ -1407,8 +1713,7 @@ void App::ModelOptionsGui() { ImGui::SetNextItemWidth(combo_w); if (ImGui::BeginCombo(camera_text, cam_name.c_str())) { auto select = [&](int idx) { - std::string name = - platform::GetCameraName(model(), camera_, idx); + std::string name = platform::GetCameraName(model(), camera_, idx); if (ImGui::Selectable(name.c_str(), (ui_.camera_idx == idx))) { ui_.camera_idx = platform::SetCamera(model(), &camera_, idx); } @@ -1461,12 +1766,11 @@ void App::ModelOptionsGui() { std::snprintf(copy_cam_label, sizeof(copy_cam_label), "%s Copy Camera", platform::ICON_FA_COPY); if (ImGui::Button(copy_cam_label)) { - std::string camera_string = - platform::CameraToString(data(), &camera_); + std::string camera_string = platform::CameraToString(data(), &camera_); platform::MaybeSaveToClipboard(camera_string); } - ImGui::SetItemTooltip("%s", - "Copy the current camera pose as an MJCF XML string"); + ImGui::SetItemTooltip( + "%s", "Copy the current camera pose as an MJCF XML string"); } ImGui::Spacing(); @@ -1646,7 +1950,8 @@ void App::SpecEditorGui() { ImGui::TableNextColumn(); bool is_dark = ImGui::GetStyle().Colors[ImGuiCol_WindowBg].x < 0.5f; - ImColor compile_green = is_dark ? ImColor(40, 125, 60, 255) : ImColor(40, 180, 40, 255); + ImColor compile_green = + is_dark ? ImColor(40, 125, 60, 255) : ImColor(40, 180, 40, 255); ImGui::PushStyleColor(ImGuiCol_Button, compile_green.Value); if (ImGui::Button("Compile and Reload", ImVec2(-1, 0))) { pending_op_ = [this]() { @@ -1862,15 +2167,16 @@ void App::ToolBarGui() { const float label_width = platform::GetExpectedLabelWidth(); const float copy_btn_width = ImGui::GetFrameHeight(); const float sp = ImGui::GetStyle().ItemSpacing.x; - const float right_width = copy_btn_width + label_width + sp + - label_width + sp + label_width; + const float right_width = + copy_btn_width + label_width + sp + label_width + sp + label_width; const float separator_width = ImGui::GetFrameHeight() * .6f; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, right_width); ImGui::TableNextColumn(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().WindowPadding.x); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + + ImGui::GetStyle().WindowPadding.x); const float btn_size = ImGui::GetFrameHeight(); const ImVec2 square_size(btn_size, btn_size); @@ -1914,7 +2220,6 @@ void App::ToolBarGui() { ImGui::SameLine(); platform::FrameSelectionGui(&vis_options_); - ImGui::EndTable(); } ImGui::PopStyleVar(); @@ -1948,7 +2253,9 @@ void App::StatusBarGui() { } void App::MainMenuGui() { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(12.0f * ImGui::GetStyle().FontScaleDpi, ImGui::GetStyle().ItemSpacing.y)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, + ImVec2(12.0f * ImGui::GetStyle().FontScaleDpi, + ImGui::GetStyle().ItemSpacing.y)); if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { #ifndef __EMSCRIPTEN__ @@ -2047,12 +2354,11 @@ void App::MainMenuGui() { tmp_.inspector_panel = true; } } - if (ImGui::MenuItem( - tmp_.toolbar ? "Hide Toolbar" : "Show Toolbar")) { + if (ImGui::MenuItem(tmp_.toolbar ? "Hide Toolbar" : "Show Toolbar")) { tmp_.toolbar = !tmp_.toolbar; } - if (ImGui::MenuItem( - tmp_.status_bar ? "Hide Status Bar" : "Show Status Bar")) { + if (ImGui::MenuItem(tmp_.status_bar ? "Hide Status Bar" + : "Show Status Bar")) { tmp_.status_bar = !tmp_.status_bar; } if (ImGui::MenuItem("Full Screen", "F11")) { diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index f0441298..a66d5e82 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -164,6 +164,13 @@ class App { int state_sig = 0; std::vector state; + // Timeline: simulation time recorded at history index 0 (the head). + double sim_head_time_ = 0.0; + float timeline_lh_width = 0.0f; + float timeline_rh_width = 0.0f; + bool scrubber_active = false; + float scrubber_grab_offset = 0.0f; + // Picture-in-Picture. std::vector pips;