Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,10 @@ the factors of the two axes it shows, and the isometric wireframe in the
lower-right panel is stretched by all three, so a shallow ocean's grid boxes
stay visible there too. With a [companion
plotfile](#companion-plotfiles) open, the axis perpendicular to the shared
plane has one factor per dataset. The factors reset to 1 when you open a
new dataset or sequence and are kept while stepping through a sequence's
frames; the proportion persists across sessions.
plane has one factor per dataset. Factors range from 0.01 to 10000 and are
saved: they carry over to the datasets you open next and to later sessions,
until **Reset** puts them back to 1. The companion's own factor starts at 1
with each companion. The proportion persists across sessions too.

The stretch is applied on screen only. The slice raster keeps one sample per
finest cell, readouts and overlays follow the stretch, and image and animation
Expand All @@ -375,7 +376,8 @@ cells, in Physical Size mode equal axis factors. Vector glyphs are drawn in
cell units, so a stretched display skews their arrows.

The controls are unavailable for 2-D spherical plotfiles, whose R-Z view is
already physical, and Physical Size is unavailable for standalone FABs and
already physical; saved axis factors do not apply to them in any layout and
wait for the next plotfile. Physical Size is unavailable for standalone FABs and
MultiFabs, which carry no cell sizes. While a [mapped grid](#mapped-grids) is
shown the display is physical too: the menu shows Physical Size in effect,
Axis Scaling still applies, and the persisted proportion is untouched.
Expand Down Expand Up @@ -923,8 +925,8 @@ their neutral gray under every skin, so a colormap looks the same whichever
one you pick.

Window geometry, logarithmic mapping, palette, skin, number format,
animation speed, aspect ratio proportion, the mapped-grid display, and the
isosurface color persist across sessions.
animation speed, aspect ratio proportion, axis scaling factors, the
mapped-grid display, and the isosurface color persist across sessions.

Each open dataset has a 1 GiB data cache by default, and volume rendering fills
a second cache of the same size with the grids it samples the field into (an
Expand Down
4 changes: 4 additions & 0 deletions src/qt/AspectMode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ enum class AspectMode : int {
PhysicalSize = 1,
};

// The range an Axis Scaling factor may take, in the dialog and from settings.
inline constexpr double minimumAxisScale = 0.01;
inline constexpr double maximumAxisScale = 10000.0;

// The display stretch of each dataset axis: the user's factor times, in
// PhysicalSize mode, the finest level's cell size. The result is normalized
// so the smallest factor over the dataset's dimensions is one, which makes a
Expand Down
6 changes: 5 additions & 1 deletion src/qt/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1683,8 +1683,12 @@ std::array<double, 3> MainWindow::displayStretchPerAxis() const
// (updateAspectControls shows the mode in effect). Only the spherical R-Z
// warp is physical in its pixels.
const auto mode = displayIsMapped() ? AspectMode::PhysicalSize : m_aspectMode;
// Axis Scaling is unavailable for a spherical plotfile, so its saved
// factors wait for the next dataset rather than stretch r and theta.
const auto factors = displayIsSpherical()
? std::array<double, 3>{1.0, 1.0, 1.0} : m_axisScale;
return amrvis::qt::displayStretchPerAxis(primary().session->metadata(),
mode, m_axisScale, displayIsSpherical());
mode, factors, displayIsSpherical());
}

std::array<double, 2> MainWindow::displayStretchFor(
Expand Down
9 changes: 6 additions & 3 deletions src/qt/MainWindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ class MainWindow final : public QMainWindow {
{
applyAxisScale(axisScale);
}
[[nodiscard]] std::array<double, 3> axisScaleForTest() const { return m_axisScale; }
void showAxisScalingDialogForTest() { showAxisScalingDialog(); }
[[nodiscard]] bool aspectMenuEnabledForTest() const;
[[nodiscard]] double activeViewStretchRatioForTest() const;
// The isometric view's outlined domain in its display coordinates.
Expand Down Expand Up @@ -1243,14 +1245,14 @@ class MainWindow final : public QMainWindow {
void showLengthUnitsDialog();
void applyLengthUnit(const QString& unitId);
// View > Aspect Ratio: the per-axis display stretch (see AspectMode.hpp).
// The dialog edits m_axisScale; applyAxisScale installs a new set and
// resetAxisScale returns to unit factors when a dataset is opened.
// The dialog edits m_axisScale; applyAxisScale installs a new set, saved
// in the settings and kept across opens.
void showAxisScalingDialog();
// The per-axis factors (the primary's along every axis) and, with a
// companion, the companion's factor along the perpendicular axis.
void applyAxisScale(const std::array<double, 3>& axisScale,
std::optional<double> companionPerpendicularScale = std::nullopt);
void resetAxisScale();
void closeAxisScalingDialog();
void setAspectMode(AspectMode mode);
[[nodiscard]] std::array<double, 3> displayStretchPerAxis() const;
// The two factors a panel shows, normalized so the smaller is one.
Expand Down Expand Up @@ -1331,6 +1333,7 @@ class MainWindow final : public QMainWindow {
void updateWindowTitle();
void restoreSettings();
void saveSettings();
void saveAxisScale();

// Per-view wiring and display updates. A panel's ImageView is wired once
// for the signals that belong to the panel (zoom, fit, resize, scroll,
Expand Down
25 changes: 24 additions & 1 deletion src/qt/MainWindowDataset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ namespace amrvis::qt {

namespace {

QString axisScaleKey(std::size_t axis)
{
constexpr std::array<const char*, 3> keys{
"aspect/axisScaleX", "aspect/axisScaleY", "aspect/axisScaleZ"};
return QString::fromLatin1(keys[axis]);
}

struct ExportChoices {
bool colorBar;
bool axes;
Expand Down Expand Up @@ -231,6 +238,12 @@ void MainWindow::restoreSettings()
}
}
}
for (std::size_t axis = 0; axis < 3; ++axis) {
bool ok = false;
const auto value = settings.value(axisScaleKey(axis), 1.0).toDouble(&ok);
m_axisScale[axis] = ok && std::isfinite(value) && value > 0.0
? std::clamp(value, minimumAxisScale, maximumAxisScale) : 1.0;
}
applySpeed();

const auto geometry = settings.value(QStringLiteral("geometry")).toByteArray();
Expand Down Expand Up @@ -271,6 +284,16 @@ void MainWindow::saveSettings()
settings.setValue(QStringLiteral("mappedGrid/enabled"), m_mappedGrid);
}

void MainWindow::saveAxisScale()
{
// Written only when the factors change, not by saveSettings: another
// window's unrelated save would put back its own factors.
auto settings = makeSettings();
for (std::size_t axis = 0; axis < 3; ++axis) {
settings.setValue(axisScaleKey(axis), m_axisScale[axis]);
}
}

void MainWindow::updateWindowTitle()
{
if (!primary().openMetadata) {
Expand Down Expand Up @@ -948,7 +971,7 @@ void MainWindow::openDatasetImpl(const std::filesystem::path& path,
closeSequence();
resetRangeState();
resetLengthUnit();
resetAxisScale();
closeAxisScalingDialog();
closeCompanion();
// The new dataset arrives fitted -- setPlaceholder below puts every view
// back to Fit -- so the scale report has to come back with it. Without
Expand Down
35 changes: 17 additions & 18 deletions src/qt/MainWindowInteraction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,24 +256,26 @@ void MainWindow::showAxisScalingDialog()

auto* explanation = new QLabel(tr(
"Stretch each axis of the slice views by a factor. Factors apply on "
"top of the Aspect Ratio mode and reset when a dataset is opened."),
"top of the Aspect Ratio mode and are kept for later datasets and "
"sessions."),
dialog);
explanation->setWordWrap(true);
const int dimension = primary().session ? primary().session->metadata().dimension : 3;
auto* form = new QFormLayout;
std::array<QDoubleSpinBox*, 3> spins{nullptr, nullptr, nullptr};
std::array<ScientificDoubleSpinBox*, 3> spins{nullptr, nullptr, nullptr};
// With a companion, the axis perpendicular to the shared plane has a
// second factor for the companion, so each dataset can be stretched on
// its own; the primary's is the ordinary axis factor.
QDoubleSpinBox* companionSpin = nullptr;
ScientificDoubleSpinBox* companionSpin = nullptr;
const std::array<QString, 3> names{tr("X"), tr("Y"), tr("Z")};
const std::array<const char*, 3> objectNames{
"axisScaleSpinX", "axisScaleSpinY", "axisScaleSpinZ"};
// Shortest form ("1", "0.125", "1000"), so a factor is typed without
// deleting trailing zeros; "1e3" works too.
const auto makeSpin = [dialog](const char* objectName, double value, bool enabled) {
auto* spin = new QDoubleSpinBox(dialog);
auto* spin = new ScientificDoubleSpinBox(dialog);
spin->setObjectName(QLatin1String(objectName));
spin->setDecimals(3);
spin->setRange(0.01, 100.0);
spin->setRange(minimumAxisScale, maximumAxisScale);
spin->setSingleStep(0.1);
spin->setValue(value);
spin->setEnabled(enabled);
Expand Down Expand Up @@ -355,7 +357,8 @@ void MainWindow::applyAxisScale(const std::array<double, 3>& axisScale,
for (std::size_t axis = 0; axis < 3; ++axis) {
factors[axis] = sane(axisScale[axis]);
}
bool changed = factors != m_axisScale;
const bool axisChanged = factors != m_axisScale;
bool changed = axisChanged;
m_axisScale = factors;
if (companionPerpendicularScale) {
const auto value = sane(*companionPerpendicularScale);
Expand All @@ -366,23 +369,19 @@ void MainWindow::applyAxisScale(const std::array<double, 3>& axisScale,
clearNavigation();
applyDisplayStretches();
}
// The axis factors persist; the companion's own factor belongs to it.
if (axisChanged) {
saveAxisScale();
}
}

void MainWindow::resetAxisScale()
void MainWindow::closeAxisScalingDialog()
{
// Axis factors belong to this dataset, including any unapplied edit.
// The factors stay, but an open dialog's rows (and unapplied edits) belong
// to the outgoing dataset and its companion.
if (m_axisScalingDialog != nullptr) {
m_axisScalingDialog->reject();
}
m_axisScale = {1.0, 1.0, 1.0};
// The views still show the outgoing dataset, and keep showing it if the
// new one fails to load, so they take the unit factors now. No remote
// re-request: that dataset is on its way out.
for (auto* state : currentViews()) {
applyDisplayStretch(*state);
}
updateScaleBarAvailability();
updateScaleBars();
}

void MainWindow::validateVectorMode()
Expand Down
2 changes: 1 addition & 1 deletion src/qt/MainWindowSlice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2349,7 +2349,7 @@ void MainWindow::prepareSequence(std::size_t frameCount)
closeSequence();
resetRangeState();
resetLengthUnit();
resetAxisScale();
closeAxisScalingDialog();
closeCompanion();
m_fabNavigator->reset();
m_particleController->cancel();
Expand Down
42 changes: 42 additions & 0 deletions src/qt/ScientificDoubleSpinBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
#include <QDoubleValidator>
#include <QLineEdit>
#include <QLocale>
#include <QRegularExpression>
#include <QSignalBlocker>

#include <algorithm>
#include <cmath>
#include <limits>

namespace amrvis::qt {
Expand All @@ -31,6 +33,16 @@ QValidator::State validateNumber(const QString& text, int position,
return validator.validate(candidate, position);
}

// A whole decimal number in either locale's notation. QLocale::toDouble
// rejects one only when it underflows.
bool isCompleteNumber(const QString& text, const QLocale& locale)
{
const QRegularExpression pattern(QStringLiteral(
"^[+-]?(\\d+(%1\\d*)?|%1\\d+)([eE][+-]?\\d+)?$")
.arg(QRegularExpression::escape(locale.decimalPoint())));
return pattern.match(text).hasMatch();
}

} // namespace

ScientificDoubleSpinBox::ScientificDoubleSpinBox(QWidget* parent)
Expand Down Expand Up @@ -99,6 +111,36 @@ double ScientificDoubleSpinBox::valueFromText(const QString& text) const
return ok ? localizedValue : QDoubleSpinBox::valueFromText(text);
}

void ScientificDoubleSpinBox::fixup(QString& input) const
{
// A number past the range is clamped to it. Left alone, Qt puts the old
// value back without a word. An overflow parses as an infinity, and a
// complete number that still fails to parse has underflowed to zero.
const auto number = numberText(input);
const auto parse = [&number](const QLocale& locale, double& value) {
bool ok = false;
value = locale.toDouble(number, &ok);
if (!ok && !std::isinf(value) && isCompleteNumber(number, locale)) {
value = 0.0;
ok = true;
}
return ok || std::isinf(value);
};
double value = 0.0;
if ((!parse(cNumberLocale(), value) && !parse(locale(), value)) || std::isnan(value)) {
QDoubleSpinBox::fixup(input);
return;
}
// Full precision, not the display format: the text is parsed again, and a
// short format can round the bound past itself (DBL_MAX to 1.8e+308). No
// group separator either: a comma-decimal locale reads 10,000 as 10. The
// committed value is then shown in the display format.
input = prefix()
+ QString::number(std::clamp(value, minimum(), maximum()), 'g',
std::numeric_limits<double>::max_digits10)
+ suffix();
}

QValidator::State ScientificDoubleSpinBox::validate(
QString& input, int& position) const
{
Expand Down
1 change: 1 addition & 0 deletions src/qt/ScientificDoubleSpinBox.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ScientificDoubleSpinBox : public QDoubleSpinBox {
protected:
[[nodiscard]] QString textFromValue(double value) const override;
[[nodiscard]] double valueFromText(const QString& text) const override;
void fixup(QString& input) const override;
QValidator::State validate(QString& input, int& position) const override;

private:
Expand Down
Loading
Loading