From acc6afca94b1fd27b39bef67daa881ab53cb2a3f Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sun, 14 Jan 2024 01:06:58 -0800 Subject: [PATCH] make scalar minmax limits persistent (#250) --- include/polyscope/persistent_value.h | 27 +++++++++--- include/polyscope/scalar_quantity.h | 3 +- include/polyscope/scalar_quantity.ipp | 63 +++++++++++++++++++-------- src/volume_grid_scalar_quantity.cpp | 4 +- test/src/misc_test.cpp | 25 +++++++++++ 5 files changed, 94 insertions(+), 28 deletions(-) diff --git a/include/polyscope/persistent_value.h b/include/polyscope/persistent_value.h index 96f08b8a..fc7dca8f 100644 --- a/include/polyscope/persistent_value.h +++ b/include/polyscope/persistent_value.h @@ -46,14 +46,13 @@ class PersistentValue { PersistentValue(const std::string& name_, T value_) : name(name_), value(value_) { if (detail::getPersistentCacheRef().cache.find(name) != detail::getPersistentCacheRef().cache.end()) { value = detail::getPersistentCacheRef().cache[name]; - holdsDefaultValue = false; + holdsDefaultValue_ = false; } else { // Update cache value detail::getPersistentCacheRef().cache[name] = value; } } - // Ensure in cache on deletion (see not above reference conversion) ~PersistentValue() {} // Don't want copy or move constructors, only operators @@ -85,32 +84,48 @@ class PersistentValue { // NOTE if you write via this reference, the value will not _actually_ be cached until // manuallyChanged() is called, rather than immediately (ugly, but seems necessary to use with imgui) T& get() { return value; } + + // Mark that a value has been directly written via the get() reference, and should be cached void manuallyChanged() { set(value); } + // clears any cached value, but does not change the current value of the variable + void clearCache() { + detail::getPersistentCacheRef().cache.erase(name); + holdsDefaultValue_ = true; + } + // Explicit setter, which takes care of storing in cache void set(T value_) { value = value_; detail::getPersistentCacheRef().cache[name] = value; - holdsDefaultValue = false; + holdsDefaultValue_ = false; } // Passive setter, will change value without marking in cache; does nothing if some value has already been directly // set (equivalent to constructing with a different value). void setPassive(T value_) { - if (holdsDefaultValue) { + if (holdsDefaultValue_) { value = value_; detail::getPersistentCacheRef().cache[name] = value; } } + bool holdsDefaultValue() const { return holdsDefaultValue_; } + // Make all template variants friends, so conversion can access private members template friend class PersistentValue; +protected: + // the name of the value const std::string name; + + // the value T value; - bool holdsDefaultValue = true; // True if the value was set on construction and never changed. False if it was pulled - // from cache or has ever been explicitly set + + // True if the value was set on construction or passively and never changed. False if + // it was pulled from cache or has ever been explicitly set + bool holdsDefaultValue_ = true; }; // clang-format off diff --git a/include/polyscope/scalar_quantity.h b/include/polyscope/scalar_quantity.h index 167eb3bc..e999d039 100644 --- a/include/polyscope/scalar_quantity.h +++ b/include/polyscope/scalar_quantity.h @@ -67,8 +67,9 @@ class ScalarQuantity { // === Visualization parameters // Affine data maps and limits - std::pair vizRange; // TODO make these persistent std::pair dataRange; + PersistentValue vizRangeMin; + PersistentValue vizRangeMax; Histogram hist; // Parameters diff --git a/include/polyscope/scalar_quantity.ipp b/include/polyscope/scalar_quantity.ipp index 550dba2d..0c5fba9d 100644 --- a/include/polyscope/scalar_quantity.ipp +++ b/include/polyscope/scalar_quantity.ipp @@ -8,6 +8,8 @@ template ScalarQuantity::ScalarQuantity(QuantityT& quantity_, const std::vector& values_, DataType dataType_) : quantity(quantity_), values(&quantity, quantity.uniquePrefix() + "values", valuesData), valuesData(values_), dataType(dataType_), dataRange(robustMinMax(values.data, 1e-5)), + vizRangeMin(quantity.uniquePrefix() + "vizRangeMin", -777.), // set later, + vizRangeMax(quantity.uniquePrefix() + "vizRangeMax", -777.), // including clearing cache cMap(quantity.uniquePrefix() + "cmap", defaultColorMap(dataType)), isolinesEnabled(quantity.uniquePrefix() + "isolinesEnabled", false), isolineWidth(quantity.uniquePrefix() + "isolineWidth", @@ -17,7 +19,12 @@ ScalarQuantity::ScalarQuantity(QuantityT& quantity_, const std::vecto { hist.updateColormap(cMap.get()); hist.buildHistogram(values.data); - resetMapRange(); + + if (vizRangeMin.holdsDefaultValue()) { // min and max should always have same cache state + // dynamically compute a viz range from the data min/max + // note that this also clears the persistent value's cahce, so it's like it was never set + resetMapRange(); + } } template @@ -62,7 +69,7 @@ void ScalarQuantity::buildScalarUI() { // Draw the histogram of values - hist.colormapRange = vizRange; + hist.colormapRange = std::pair(vizRangeMin.get(), vizRangeMax.get()); float windowWidth = ImGui::GetWindowWidth(); float histWidth = 0.75 * windowWidth; hist.buildUI(histWidth); @@ -77,36 +84,47 @@ void ScalarQuantity::buildScalarUI() { float imPad = ImGui::GetStyle().ItemSpacing.x; ImGui::PushItemWidth((histWidth - imPad) / 2); float speed = (dataRange.second - dataRange.first) / 100.; + bool changed = false; switch (dataType) { case DataType::STANDARD: { - ImGui::DragFloat("##min", &vizRange.first, speed, dataRange.first, vizRange.second, "%.5g", - ImGuiSliderFlags_NoRoundToFormat); + changed = changed || ImGui::DragFloat("##min", &vizRangeMin.get(), speed, dataRange.first, vizRangeMax.get(), + "%.5g", ImGuiSliderFlags_NoRoundToFormat); ImGui::SameLine(); - ImGui::DragFloat("##max", &vizRange.second, speed, vizRange.first, dataRange.second, "%.5g", - ImGuiSliderFlags_NoRoundToFormat); + changed = changed || ImGui::DragFloat("##max", &vizRangeMax.get(), speed, vizRangeMin.get(), dataRange.second, + "%.5g", ImGuiSliderFlags_NoRoundToFormat); } break; case DataType::SYMMETRIC: { float absRange = std::max(std::abs(dataRange.first), std::abs(dataRange.second)); - if (ImGui::DragFloat("##min", &vizRange.first, speed, -absRange, 0.f, "%.5g", ImGuiSliderFlags_NoRoundToFormat)) { - vizRange.second = -vizRange.first; + if (ImGui::DragFloat("##min", &vizRangeMin.get(), speed, -absRange, 0.f, "%.5g", + ImGuiSliderFlags_NoRoundToFormat)) { + vizRangeMax.get() = -vizRangeMin.get(); + changed = true; } ImGui::SameLine(); - if (ImGui::DragFloat("##max", &vizRange.second, speed, 0.f, absRange, "%.5g", ImGuiSliderFlags_NoRoundToFormat)) { - vizRange.first = -vizRange.second; + if (ImGui::DragFloat("##max", &vizRangeMax.get(), speed, 0.f, absRange, "%.5g", + ImGuiSliderFlags_NoRoundToFormat)) { + vizRangeMin.get() = -vizRangeMax.get(); + changed = true; } } break; case DataType::MAGNITUDE: { - ImGui::DragFloat("##max", &vizRange.second, speed, 0.f, dataRange.second, "%.5g", - ImGuiSliderFlags_NoRoundToFormat); + changed = changed || ImGui::DragFloat("##max", &vizRangeMax.get(), speed, 0.f, dataRange.second, "%.5g", + ImGuiSliderFlags_NoRoundToFormat); } break; } + if (changed) { + vizRangeMin.manuallyChanged(); + vizRangeMax.manuallyChanged(); + requestRedraw(); + } + ImGui::PopItemWidth(); } @@ -162,8 +180,8 @@ std::vector ScalarQuantity::addScalarRules(std::vector void ScalarQuantity::setScalarUniforms(render::ShaderProgram& p) { - p.setUniform("u_rangeLow", vizRange.first); - p.setUniform("u_rangeHigh", vizRange.second); + p.setUniform("u_rangeLow", vizRangeMin.get()); + p.setUniform("u_rangeHigh", vizRangeMax.get()); if (isolinesEnabled.get()) { p.setUniform("u_modLen", getIsolineWidth()); @@ -175,17 +193,23 @@ template QuantityT* ScalarQuantity::resetMapRange() { switch (dataType) { case DataType::STANDARD: - vizRange = dataRange; + vizRangeMin = dataRange.first; + vizRangeMax = dataRange.second; break; case DataType::SYMMETRIC: { double absRange = std::max(std::abs(dataRange.first), std::abs(dataRange.second)); - vizRange = std::make_pair(-absRange, absRange); + vizRangeMin = -absRange; + vizRangeMax = absRange; } break; case DataType::MAGNITUDE: - vizRange = std::make_pair(0., dataRange.second); + vizRangeMin = 0.; + vizRangeMax = dataRange.second; break; } + vizRangeMin.clearCache(); + vizRangeMax.clearCache(); + requestRedraw(); return &quantity; } @@ -214,13 +238,14 @@ std::string ScalarQuantity::getColorMap() { template QuantityT* ScalarQuantity::setMapRange(std::pair val) { - vizRange = val; + vizRangeMin = val.first; + vizRangeMax = val.second; requestRedraw(); return &quantity; } template std::pair ScalarQuantity::getMapRange() { - return vizRange; + return std::pair(vizRangeMin.get(), vizRangeMax.get()); } template std::pair ScalarQuantity::getDataRange() { diff --git a/src/volume_grid_scalar_quantity.cpp b/src/volume_grid_scalar_quantity.cpp index 58009269..df928ee1 100644 --- a/src/volume_grid_scalar_quantity.cpp +++ b/src/volume_grid_scalar_quantity.cpp @@ -71,9 +71,9 @@ void VolumeGridNodeScalarQuantity::buildCustomUI() { // Set isovalue ImGui::PushItemWidth(120); - if (ImGui::SliderFloat("##Radius", &isosurfaceLevel.get(), vizRange.first, vizRange.second, "%.4e")) { + if (ImGui::SliderFloat("##Radius", &isosurfaceLevel.get(), vizRangeMin.get(), vizRangeMax.get(), "%.4e")) { // Note: we intentionally do this rather than calling setIsosurfaceLevel(), because that function immediately - // recomputes the level set mesh, which is too expensive during user interaction + // recomputes the levelset mesh, which is too expensive during user interaction isosurfaceLevel.manuallyChanged(); } ImGui::PopItemWidth(); diff --git a/test/src/misc_test.cpp b/test/src/misc_test.cpp index 4ccfe9ee..6862cd3c 100644 --- a/test/src/misc_test.cpp +++ b/test/src/misc_test.cpp @@ -2,6 +2,31 @@ #include "polyscope_test.h" +// ============================================================ +// =============== Scalar Quantity Tests +// ============================================================ + +// We test these on a point cloud because it is convenient, but really we are testing the scalar quantity + +TEST_F(PolyscopeTest, TestScalarQuantity) { + auto psPoints = registerPointCloud(); + + std::vector vScalar(psPoints->nPoints(), 7.); + auto q1 = psPoints->addScalarQuantity("vScalar", vScalar); + q1->setEnabled(true); + polyscope::show(3); + + // get map range + std::pair newRange = {-1., 1.}; + q1->setMapRange(newRange); + EXPECT_EQ(newRange, q1->getMapRange()); + + + polyscope::show(3); + + polyscope::removeAllStructures(); +} + // ============================================================ // =============== Materials tests // ============================================================