Implement viscous pause in Studio

Viscous pause is like pause but the simulation continues with zero gravity, high viscosity and passive springs disabled. It is useful to move things like cabinets with shelves---just move the shelf and the drawers come along as well.

* Updated the simulation control widgets
* Added Ctrl-space shortcut to toggle play/viscous pause

PiperOrigin-RevId: 869810053
Change-Id: Icf649e1f2631c2a64a5d2f25af9537b6a12bdf2c
This commit is contained in:
Matija Kecman
2026-02-13 11:10:22 -08:00
committed by Copybara-Service
parent 5274e5f720
commit 1808079c62
6 changed files with 200 additions and 69 deletions
+1 -1
View File
@@ -627,7 +627,7 @@ void PhysicsGui(mjModel* model, float min_width) {
ImGui_InputN("Wind", opt.wind, 3); ImGui_InputN("Wind", opt.wind, 3);
ImGui_InputN("Magnetic", opt.magnetic, 3); ImGui_InputN("Magnetic", opt.magnetic, 3);
ImGui_Input("Density", &opt.density, {.min = .1, .max = 1}); ImGui_Input("Density", &opt.density, {.min = .1, .max = 1});
ImGui_Input("Viscosity", &opt.viscosity, {.min = .1, .max = 1}); ImGui_Input("Viscosity", &opt.viscosity, {.min = .1, .max = 10});
ImGui_Input("Imp Ratio", &opt.impratio, {.min = .1, .max = 1}); ImGui_Input("Imp Ratio", &opt.impratio, {.min = .1, .max = 1});
ImGui::TreePop(); ImGui::TreePop();
}; };
+19
View File
@@ -39,6 +39,7 @@ static constexpr const char ICON_FA_DIAMOND[] = "\xEF\x88\x99";
static constexpr const char ICON_FA_EJECT[] = "\xEF\x81\x92"; static constexpr const char ICON_FA_EJECT[] = "\xEF\x81\x92";
static constexpr const char ICON_FA_FAST_FORWARD[] = "\xEF\x81\x90"; static constexpr const char ICON_FA_FAST_FORWARD[] = "\xEF\x81\x90";
static constexpr const char ICON_FA_MOON[] = "\xEF\x86\x86"; static constexpr const char ICON_FA_MOON[] = "\xEF\x86\x86";
static constexpr const char ICON_FA_MAGIC[] = "\xEF\x83\x90";
static constexpr const char ICON_FA_PAUSE[] = "\xEF\x81\x8C"; static constexpr const char ICON_FA_PAUSE[] = "\xEF\x81\x8C";
static constexpr const char ICON_FA_PLAY[] = "\xEF\x81\x8B"; static constexpr const char ICON_FA_PLAY[] = "\xEF\x81\x8B";
static constexpr const char ICON_FA_REFRESH[] = "\xEF\x80\xA1"; static constexpr const char ICON_FA_REFRESH[] = "\xEF\x80\xA1";
@@ -262,6 +263,24 @@ inline bool ImGui_IsChordJustPressed(ImGuiKeyChord chord) {
return ImGui::IsKeyChordPressed(chord, 0); return ImGui::IsKeyChordPressed(chord, 0);
} }
// Stateful button that displays the given color when active, and shows a
// semi-transparent hover color (controlled by hover_alpha) when inactive.
inline bool ImGui_ColorButton(const char* label, bool active, ImColor color,
const ImVec2& size = ImVec2(0, 0),
float hover_alpha = 0.5f) {
ScopedStyle style;
const ImColor hover(color.Value.x, color.Value.y, color.Value.z,
color.Value.w * hover_alpha);
if (active) {
style.Color(ImGuiCol_Button, color);
style.Color(ImGuiCol_ButtonHovered, color);
} else {
style.Color(ImGuiCol_ButtonHovered, hover);
}
style.Color(ImGuiCol_ButtonActive, color);
return ImGui::Button(label, size);
}
// Begin a boxed section with outer borders - use EndBoxSection to close. // Begin a boxed section with outer borders - use EndBoxSection to close.
inline bool BeginBoxSection(const char* id, ImGuiTableFlags extra_flags = 0) { inline bool BeginBoxSection(const char* id, ImGuiTableFlags extra_flags = 0) {
ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | extra_flags; ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | extra_flags;
+35 -17
View File
@@ -13,10 +13,12 @@
// limitations under the License. // limitations under the License.
#include "experimental/platform/step_control.h" #include "experimental/platform/step_control.h"
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <cstdlib> #include <cstdlib>
#include <ratio> #include <ratio>
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
namespace mujoco::platform { namespace mujoco::platform {
@@ -28,17 +30,11 @@ static mjtNum Timer() {
return Milliseconds(Clock::now() - start).count(); return Milliseconds(Clock::now() - start).count();
} }
StepControl::StepControl() { StepControl::StepControl() { mjcb_time = Timer; }
mjcb_time = Timer;
}
float StepControl::GetSpeedMeasured() const { float StepControl::GetSpeedMeasured() const { return speed_measured_; }
return speed_measured_;
}
float StepControl::GetSpeed() const { float StepControl::GetSpeed() const { return speed_; }
return speed_;
}
void StepControl::SetSpeed(float speed_percent_real_time) { void StepControl::SetSpeed(float speed_percent_real_time) {
speed_ = std::clamp(speed_percent_real_time, .1f, 100.f); speed_ = std::clamp(speed_percent_real_time, .1f, 100.f);
@@ -59,12 +55,34 @@ void StepControl::SetNoiseParameters(float ctrl_noise_scale,
ctrl_noise_rate_ = ctrl_noise_rate; ctrl_noise_rate_ = ctrl_noise_rate;
} }
StepControl::Status StepControl::Advance(const mjModel* m, mjData* d) { void StepControl::SetPauseState(PauseState state, mjModel* m) {
if (pause_state_ == PauseState::kViscousPaused &&
state != PauseState::kViscousPaused && m) {
mju_copy3(m->opt.gravity, saved_gravity_);
m->opt.viscosity = saved_viscosity_;
m->opt.disableflags = saved_disableflags_;
}
if (state == PauseState::kViscousPaused &&
pause_state_ != PauseState::kViscousPaused && m) {
mju_copy3(saved_gravity_, m->opt.gravity);
saved_viscosity_ = m->opt.viscosity;
saved_disableflags_ = m->opt.disableflags;
mju_zero3(m->opt.gravity);
m->opt.viscosity = 10;
m->opt.disableflags |= mjDSBL_SPRING;
}
pause_state_ = state;
}
StepControl::Status StepControl::Advance(mjModel* m, mjData* d) {
if (!m) { if (!m) {
return Status::kOk; return Status::kOk;
} }
if (paused_) {
if (pause_state_ == PauseState::kNormalPaused) {
// When we eventually unpause, we need to make sure we sync to immediately // When we eventually unpause, we need to make sure we sync to immediately
// and step once. Without this we could step many times before rendering // and step once. Without this we could step many times before rendering
// resulting in a noticeable delay before the simulation restarts // resulting in a noticeable delay before the simulation restarts
@@ -151,7 +169,7 @@ StepControl::Status StepControl::Advance(const mjModel* m, mjData* d) {
for (mjtWarning w : kDivergedWarnings) { for (mjtWarning w : kDivergedWarnings) {
if (d->warning[w].number > 0) { if (d->warning[w].number > 0) {
// Stop stepping if the simulation diverged. // Stop stepping if the simulation diverged.
paused_ = true; pause_state_ = PauseState::kNormalPaused;
return Status::kDiverged; return Status::kDiverged;
} }
} }
@@ -179,7 +197,7 @@ void StepControl::InjectNoise(const mjModel* m, mjData* d) {
// convert rate and scale to discrete time (Ornstein–Uhlenbeck) // convert rate and scale to discrete time (Ornstein–Uhlenbeck)
mjtNum rate = mju_exp(-m->opt.timestep / ctrl_noise_rate_); mjtNum rate = mju_exp(-m->opt.timestep / ctrl_noise_rate_);
mjtNum scale = ctrl_noise_std_ * mju_sqrt(1-rate*rate); mjtNum scale = ctrl_noise_std_ * mju_sqrt(1 - rate * rate);
for (int i = 0; i < m->nu; i++) { for (int i = 0; i < m->nu; i++) {
mjtNum bottom = 0; mjtNum bottom = 0;
@@ -187,14 +205,14 @@ void StepControl::InjectNoise(const mjModel* m, mjData* d) {
mjtNum midpoint = 0; mjtNum midpoint = 0;
mjtNum halfrange = 1; mjtNum halfrange = 1;
if (m->actuator_ctrllimited[i]) { if (m->actuator_ctrllimited[i]) {
bottom = m->actuator_ctrlrange[2*i]; bottom = m->actuator_ctrlrange[2 * i];
top = m->actuator_ctrlrange[2*i+1]; top = m->actuator_ctrlrange[2 * i + 1];
midpoint = 0.5 * (top + bottom); // target of exponential decay midpoint = 0.5 * (top + bottom); // target of exponential decay
halfrange = 0.5 * (top - bottom); // scales noise halfrange = 0.5 * (top - bottom); // scales noise
} }
// exponential convergence to midpoint at ctrl_noise_rate // exponential convergence to midpoint at ctrl_noise_rate
d->ctrl[i] = rate * d->ctrl[i] + (1-rate) * midpoint; d->ctrl[i] = rate * d->ctrl[i] + (1 - rate) * midpoint;
// add noise // add noise
d->ctrl[i] += scale * halfrange * mju_standardNormal(nullptr); d->ctrl[i] += scale * halfrange * mju_standardNormal(nullptr);
+18 -9
View File
@@ -36,6 +36,10 @@ class StepControl {
// Simulation was not stepped because it is paused. // Simulation was not stepped because it is paused.
kPaused, kPaused,
// Simulation is viscously paused (stepping with zero gravity, high
// viscosity, and no spring forces).
kViscousPaused,
// Simulation diverged with autoreset enabled. // Simulation diverged with autoreset enabled.
kAutoReset, kAutoReset,
@@ -49,7 +53,7 @@ class StepControl {
mjWARN_BADQACC, mjWARN_BADQVEL, mjWARN_BADQPOS}; mjWARN_BADQACC, mjWARN_BADQVEL, mjWARN_BADQPOS};
// Steps physics forward, respecting speed settings and refresh budget. // Steps physics forward, respecting speed settings and refresh budget.
Status Advance(const mjModel* m, mjData* d); Status Advance(mjModel* m, mjData* d);
// Ensures the next call to Advance() will synchronize time and step once. // Ensures the next call to Advance() will synchronize time and step once.
void ForceSync(); void ForceSync();
@@ -63,13 +67,14 @@ class StepControl {
void GetNoiseParameters(float& noise_scale, float& noise_rate) const; void GetNoiseParameters(float& noise_scale, float& noise_rate) const;
void SetNoiseParameters(float noise_scale, float noise_rate); void SetNoiseParameters(float noise_scale, float noise_rate);
// Returns true if the simulation is paused. enum class PauseState { kUnpaused, kNormalPaused, kViscousPaused };
bool IsPaused() { return paused_; }
// Pauses/unpauses the simulation. // Sets the pause state of the simulation.
void Pause() { paused_ = true; } // m must be non-null for viscous pausing.
void Unpause() { paused_ = false; } void SetPauseState(PauseState state, mjModel* m = nullptr);
void TogglePause() { paused_ = !paused_; }
// Gets the current pause state of the simulation.
PauseState GetPauseState() const { return pause_state_; }
// If the simulation is paused, will perform a single step on the next // If the simulation is paused, will perform a single step on the next
// Advance() call. // Advance() call.
@@ -104,8 +109,12 @@ class StepControl {
// Maximum mis-alignment before re-sync (simulation seconds) // Maximum mis-alignment before re-sync (simulation seconds)
double sync_misalign_ = .1; double sync_misalign_ = .1;
// Whether or not the simulation is paused. PauseState pause_state_ = PauseState::kUnpaused;
bool paused_ = false;
// Viscous pause state variables
mjtNum saved_gravity_[3] = {0};
mjtNum saved_viscosity_ = 0;
int saved_disableflags_ = 0;
// Perform only a single step on the next call to Advance() if the simulation // Perform only a single step on the next call to Advance() if the simulation
// is paused. // is paused.
+124 -42
View File
@@ -17,6 +17,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cfloat> #include <cfloat>
#include <cmath>
#include <cstddef> #include <cstddef>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
@@ -32,8 +33,8 @@
#include <imgui.h> #include <imgui.h>
#include <implot.h> #include <implot.h>
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
#include "experimental/platform/gui.h"
#include "experimental/platform/file_dialog.h" #include "experimental/platform/file_dialog.h"
#include "experimental/platform/gui.h"
#include "experimental/platform/helpers.h" #include "experimental/platform/helpers.h"
#include "experimental/platform/imgui_widgets.h" #include "experimental/platform/imgui_widgets.h"
#include "experimental/platform/interaction.h" #include "experimental/platform/interaction.h"
@@ -46,6 +47,8 @@
namespace mujoco::studio { namespace mujoco::studio {
using PauseState = platform::StepControl::PauseState;
static void ToggleFlag(mjtByte& flag) { flag = flag ? 0 : 1; } static void ToggleFlag(mjtByte& flag) { flag = flag ? 0 : 1; }
static void ToggleWindow(bool& window) { static void ToggleWindow(bool& window) {
@@ -66,6 +69,7 @@ static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) {
static constexpr const char* ICON_PLAY = platform::ICON_FA_PLAY; static constexpr const char* ICON_PLAY = platform::ICON_FA_PLAY;
static constexpr const char* ICON_PAUSE = platform::ICON_FA_PAUSE; static constexpr const char* ICON_PAUSE = platform::ICON_FA_PAUSE;
static constexpr const char* ICON_VISCOUS_PAUSE = platform::ICON_FA_MAGIC;
static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY; static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY;
static constexpr const char* ICON_UNLOAD_MODEL = platform::ICON_FA_EJECT; static constexpr const char* ICON_UNLOAD_MODEL = platform::ICON_FA_EJECT;
static constexpr const char* ICON_RELOAD_MODEL = platform::ICON_FA_REFRESH; static constexpr const char* ICON_RELOAD_MODEL = platform::ICON_FA_REFRESH;
@@ -101,8 +105,7 @@ static constexpr std::array<const char*, 31> kPercentRealTime = {
}; };
// clang-format on // clang-format on
App::App(Config config) App::App(Config config) : ini_path_(std::move(config.ini_path)) {
: ini_path_(std::move(config.ini_path)) {
platform::Window::Config window_config; platform::Window::Config window_config;
window_config.renderer_backend = platform::Renderer::GetBackend(); window_config.renderer_backend = platform::Renderer::GetBackend();
window_config.offscreen_mode = config.offscreen_mode; window_config.offscreen_mode = config.offscreen_mode;
@@ -173,11 +176,11 @@ void App::OnModelLoaded(std::string filename, ModelKind model_kind) {
model_path_ = std::move(filename); model_path_ = std::move(filename);
if (model_kind_ == kEmptyModel) { if (model_kind_ == kEmptyModel) {
step_control_.Unpause(); step_control_.SetPauseState(PauseState::kUnpaused);
} }
model_kind_ = model_kind; model_kind_ = model_kind;
if (model_kind_ == kEmptyModel) { if (model_kind_ == kEmptyModel) {
step_control_.Pause(); step_control_.SetPauseState(PauseState::kNormalPaused);
} }
// Reset/reinitialize everything that depends on the new mjModel. // Reset/reinitialize everything that depends on the new mjModel.
@@ -251,7 +254,7 @@ void App::UpdatePhysics() {
}); });
if (!stepped) { if (!stepped) {
if (!step_control_.IsPaused()) { if (step_control_.GetPauseState() != PauseState::kNormalPaused) {
mju_zero(data()->xfrc_applied, 6 * model()->nbody); mju_zero(data()->xfrc_applied, 6 * model()->nbody);
mjv_applyPerturbPose(model(), data(), &perturb_, 0); mjv_applyPerturbPose(model(), data(), &perturb_, 0);
mjv_applyPerturbForce(model(), data(), &perturb_); mjv_applyPerturbForce(model(), data(), &perturb_);
@@ -299,7 +302,7 @@ void App::LoadHistory(int offset) {
std::span<mjtNum> state = history_.SetIndex(offset); std::span<mjtNum> state = history_.SetIndex(offset);
if (!state.empty()) { if (!state.empty()) {
// Pause simulation when entering history mode. // Pause simulation when entering history mode.
step_control_.Pause(); step_control_.SetPauseState(PauseState::kNormalPaused);
// Load the state into the data buffer. // Load the state into the data buffer.
mj_setState(model(), data(), state.data(), mjSTATE_INTEGRATION); mj_setState(model(), data(), state.data(), mjSTATE_INTEGRATION);
@@ -540,19 +543,32 @@ void App::HandleKeyboardEvents() {
} else if (ImGui_IsChordJustPressed(ImGuiKey_Equal)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_Equal)) {
SetSpeedIndex(tmp_.speed_index - 1); SetSpeedIndex(tmp_.speed_index - 1);
} else if (ImGui_IsChordJustPressed(ImGuiKey_LeftArrow)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_LeftArrow)) {
if (step_control_.IsPaused()) { if (step_control_.GetPauseState() == PauseState::kNormalPaused) {
LoadHistory(history_.GetIndex() - 1); LoadHistory(history_.GetIndex() - 1);
} }
} else if (ImGui_IsChordJustPressed(ImGuiKey_RightArrow)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_RightArrow)) {
if (step_control_.IsPaused()) { if (step_control_.GetPauseState() == PauseState::kNormalPaused) {
if (history_.GetIndex() == 0) { if (history_.GetIndex() == 0) {
step_control_.RequestSingleStep(); step_control_.RequestSingleStep();
} else { } else {
LoadHistory(history_.GetIndex() + 1); LoadHistory(history_.GetIndex() + 1);
} }
} }
} else if (ImGui_IsChordJustPressed(ImGuiMod_Ctrl | ImGuiKey_Space)) {
if (step_control_.GetPauseState() == PauseState::kViscousPaused) {
step_control_.SetPauseState(PauseState::kUnpaused, model());
} else {
step_control_.SetPauseState(PauseState::kViscousPaused, model());
tmp_.viscous_pause_time = ImGui::GetTime();
}
} else if (ImGui_IsChordJustPressed(ImGuiKey_Space)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_Space)) {
step_control_.TogglePause(); if (step_control_.GetPauseState() == PauseState::kViscousPaused) {
step_control_.SetPauseState(PauseState::kNormalPaused, model());
} else if (step_control_.GetPauseState() == PauseState::kUnpaused) {
step_control_.SetPauseState(PauseState::kNormalPaused);
} else {
step_control_.SetPauseState(PauseState::kUnpaused);
}
} else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) {
ResetPhysics(); ResetPhysics();
} else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) {
@@ -854,14 +870,16 @@ void App::BuildGui() {
style.Var(ImGuiStyleVar_Alpha, 0.6f); style.Var(ImGuiStyleVar_Alpha, 0.6f);
if (ImGui::Begin("Stats", &tmp_.stats)) { if (ImGui::Begin("Stats", &tmp_.stats)) {
const float fps = renderer_->GetFps(); const float fps = renderer_->GetFps();
platform::StatsGui(model(), data(), step_control_.IsPaused(), fps); platform::StatsGui(
model(), data(),
step_control_.GetPauseState() == PauseState::kNormalPaused, fps);
} }
ImGui::End(); ImGui::End();
} }
// Display a drag-and-drop message if no model is loaded. // Display a drag-and-drop message if no model is loaded.
if (model_kind_ == kEmptyModel) { if (model_kind_ == kEmptyModel) {
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
const char* text = "Load model file or drag-and-drop model file here."; const char* text = "Load model file or drag-and-drop model file here.";
const float width = window_->GetWidth() * ImGui::GetWindowDpiScale(); const float width = window_->GetWidth() * ImGui::GetWindowDpiScale();
@@ -881,7 +899,7 @@ void App::BuildGui() {
ImGui::Text("%s", text); ImGui::Text("%s", text);
} }
ImGui::End(); ImGui::End();
#endif // !__EMSCRIPTEN__ #endif // !__EMSCRIPTEN__
} }
FileDialogGui(); FileDialogGui();
@@ -1159,6 +1177,7 @@ void App::HelpGui() {
ImGui::Text("Toggle Fullscreen"); ImGui::Text("Toggle Fullscreen");
ImGui::Text("Free Camera"); ImGui::Text("Free Camera");
ImGui::Text("Toggle Pause"); ImGui::Text("Toggle Pause");
ImGui::Text("Toggle Visc Pause");
ImGui::Text("Reset Sim"); ImGui::Text("Reset Sim");
ImGui::Text("Toggle Left UI"); ImGui::Text("Toggle Left UI");
ImGui::Text("Toggle Right UI"); ImGui::Text("Toggle Right UI");
@@ -1184,6 +1203,7 @@ void App::HelpGui() {
ImGui::Text("F11"); ImGui::Text("F11");
ImGui::Text("Esc"); ImGui::Text("Esc");
ImGui::Text("Spc"); ImGui::Text("Spc");
ImGui::Text("Ctrl+Spc");
ImGui::Text("Bksp"); ImGui::Text("Bksp");
ImGui::Text("Tab"); ImGui::Text("Tab");
ImGui::Text("Sh+Tab"); ImGui::Text("Sh+Tab");
@@ -1265,7 +1285,7 @@ void App::ToolBarGui() {
const float scale = ImGui::GetWindowDpiScale(); const float scale = ImGui::GetWindowDpiScale();
const float right_width = 520.f * scale; const float right_width = 520.f * scale;
const ImVec2 button_size(48.f * scale, 32.f * scale); const ImVec2 button_size(48.f * scale, 32.f * scale);
const ImVec2 play_button_size(120.f * scale, 32.f * scale); const ImVec2 play_button_size(80.f * scale, 32.f * scale);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, right_width); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, right_width);
@@ -1273,21 +1293,35 @@ void App::ToolBarGui() {
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text("%s", " "); ImGui::Text("%s", " ");
// Unload button. // Combined (Unload, Reload) widget
ImGui::SameLine(); {
style.Color(ImGuiCol_ButtonHovered, red); style.Var(ImGuiStyleVar_FrameRounding, 2.0f);
if (ImGui::Button(ICON_UNLOAD_MODEL, button_size)) {
InitEmptyModel();
}
ImGui::SetItemTooltip("%s", "Unload");
style.Reset();
// Reload button. // Unload button.
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button(ICON_RELOAD_MODEL, button_size)) { {
RequestModelReload(); const ImColor a = red;
const ImColor h(a.Value.x, a.Value.y, a.Value.z, a.Value.w * 0.6f);
style.Color(ImGuiCol_ButtonHovered, h);
style.Color(ImGuiCol_ButtonActive, a);
if (ImGui::Button(ICON_UNLOAD_MODEL, button_size)) {
InitEmptyModel();
}
ImGui::SetItemTooltip("%s", "Unload");
style.Reset();
}
// Reload button.
ImGui::SameLine(0, 0);
if (ImGui::Button(ICON_RELOAD_MODEL, button_size)) {
RequestModelReload();
}
ImGui::SetItemTooltip("%s", "Reload");
} }
ImGui::SetItemTooltip("%s", "Reload");
ImGui::SameLine(0, 0);
ImGui::Text(" ");
// Reset button. // Reset button.
ImGui::SameLine(); ImGui::SameLine();
@@ -1296,15 +1330,54 @@ void App::ToolBarGui() {
} }
ImGui::SetItemTooltip("%s", "Reset"); ImGui::SetItemTooltip("%s", "Reset");
// Play/pause button. ImGui::SameLine(0, 0);
ImGui::SameLine(); ImGui::Text(" ");
const bool paused = step_control_.IsPaused();
style.Color(ImGuiCol_Button, paused ? yellow : green); // Combined (Normal Pause, Viscous Pause, Play) widget
if (ImGui::Button(paused ? ICON_PLAY : ICON_PAUSE, play_button_size)) { {
step_control_.TogglePause(); style.Var(ImGuiStyleVar_FrameRounding, 2.0f);
// Normal pause button.
ImGui::SameLine();
ImColor paused_color = yellow;
bool paused = step_control_.GetPauseState() == PauseState::kNormalPaused;
if (platform::ImGui_ColorButton(ICON_PAUSE, paused, paused_color,
button_size)) {
if (!paused) {
step_control_.SetPauseState(PauseState::kNormalPaused, model());
}
}
ImGui::SetItemTooltip("%s", "Pause");
// Viscous pause button.
ImGui::SameLine(0, 0);
ImColor vpaused_color = green;
float t = 0.f;
bool vpaused =
step_control_.GetPauseState() == PauseState::kViscousPaused;
if (vpaused) {
t = ImGui::GetTime() - tmp_.viscous_pause_time;
t = std::sqrt(std::min(t / 0.75f, 1.0f));
vpaused_color = ImColor(ImLerp(green.Value, yellow.Value, t));
}
if (platform::ImGui_ColorButton(ICON_VISCOUS_PAUSE, vpaused,
vpaused_color, button_size,
t < 1.0f ? 1.0f : 0.5f)) {
if (!vpaused) {
step_control_.SetPauseState(PauseState::kViscousPaused, model());
tmp_.viscous_pause_time = ImGui::GetTime();
}
}
ImGui::SetItemTooltip("%s", "Viscous Pause");
// Play button.
ImGui::SameLine(0, 0);
if (platform::ImGui_ColorButton(
ICON_PLAY, step_control_.GetPauseState() == PauseState::kUnpaused,
green, button_size, 0.6f)) {
step_control_.SetPauseState(PauseState::kUnpaused, model());
}
} }
ImGui::SetItemTooltip("%s", paused ? "Play" : "Pause");
style.Reset();
ImGui::SameLine(); ImGui::SameLine();
ImGui::Text("%s", " |"); ImGui::Text("%s", " |");
@@ -1433,7 +1506,10 @@ void App::StatusBarGui() {
if (!has_model()) { if (!has_model()) {
ImGui::Text("No model loaded"); ImGui::Text("No model loaded");
} else if (step_control_.IsPaused()) { } else if (step_control_.GetPauseState() == PauseState::kViscousPaused) {
ImGui::Text("Viscous Pause");
ImGui::SetItemTooltip("Zero gravity, high viscosity, no spring forces");
} else if (step_control_.GetPauseState() == PauseState::kNormalPaused) {
ImGui::Text("Paused"); ImGui::Text("Paused");
} else { } else {
const float desired_realtime = step_control_.GetSpeed(); const float desired_realtime = step_control_.GetSpeed();
@@ -1504,7 +1580,7 @@ void App::StatusBarGui() {
void App::MainMenuGui() { void App::MainMenuGui() {
if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) { if (ImGui::BeginMenu("File")) {
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
if (ImGui::MenuItem("Open Model File", "Ctrl+O")) { if (ImGui::MenuItem("Open Model File", "Ctrl+O")) {
tmp_.file_dialog = UiTempState::FileDialog_Load; tmp_.file_dialog = UiTempState::FileDialog_Load;
} }
@@ -1526,22 +1602,28 @@ void App::MainMenuGui() {
tmp_.file_dialog = UiTempState::FileDialog_PrintData; tmp_.file_dialog = UiTempState::FileDialog_PrintData;
} }
ImGui::Separator(); ImGui::Separator();
#endif // !__EMSCRIPTEN__ #endif // !__EMSCRIPTEN__
if (ImGui::MenuItem("Unload", "Ctrl+U")) { if (ImGui::MenuItem("Unload", "Ctrl+U")) {
InitEmptyModel(); InitEmptyModel();
} }
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("Quit", "Ctrl+Q")) { if (ImGui::MenuItem("Quit", "Ctrl+Q")) {
tmp_.should_exit = true; tmp_.should_exit = true;
} }
#endif // !__EMSCRIPTEN__ #endif // !__EMSCRIPTEN__
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("Simulation")) { if (ImGui::BeginMenu("Simulation")) {
if (ImGui::MenuItem("Pause", "Space", step_control_.IsPaused())) { if (ImGui::MenuItem(
step_control_.TogglePause(); "Pause", "Space",
step_control_.GetPauseState() == PauseState::kNormalPaused)) {
if (step_control_.GetPauseState() != PauseState::kNormalPaused) {
step_control_.SetPauseState(PauseState::kNormalPaused);
} else {
step_control_.SetPauseState(PauseState::kUnpaused, model());
}
} }
if (ImGui::MenuItem("Reset", "Backspace")) { if (ImGui::MenuItem("Reset", "Backspace")) {
ResetPhysics(); ResetPhysics();
+3
View File
@@ -123,6 +123,9 @@ class App {
// Controls. // Controls.
bool perturb_active = false; bool perturb_active = false;
// Time at which viscous pause was activated, for the green→yellow button
// color animation.
double viscous_pause_time = 0;
int speed_index = 0; int speed_index = 0;
float cam_speed = 0.0f; float cam_speed = 0.0f;