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;