Measurement: Add on the fly measurement unit change (#27462)

Enables the user to change the unit of the measurement temporarily to convert to a different unit. This is useful when the user sometimes needs to get a dimension of the model in a different unit than the one it was designed in.
This commit is contained in:
Paweł Biegun
2026-02-12 06:35:54 -08:00
committed by GitHub
parent 7912f84136
commit 1752fd59f9
2 changed files with 178 additions and 13 deletions

View File

@@ -46,6 +46,10 @@
#include <QMenu>
#include <QShortcut>
#include <QToolTip>
#include <QSignalBlocker>
#include <Base/Quantity.h>
#include <array>
using namespace MeasureGui;
@@ -57,6 +61,36 @@ constexpr auto taskMeasureAutoSaveSettingsName = "AutoSave";
constexpr auto taskMeasureGreedySelection = "GreedySelection";
using SelectionStyle = Gui::SelectionSingleton::SelectionStyle;
constexpr std::array
lengthUnitLabels {"nm", "µm", "mm", "cm", "dm", "m", "km", "in", "ft", "thou", "yd", "mi"};
constexpr std::array angleUnitLabels {"deg", "rad", "gon"};
constexpr std::array areaUnitLabels {"mm²", "cm²", "", "km²", "in²", "ft²", "yd²", "mi²"};
template<std::size_t N>
QStringList toQStringList(const std::array<const char*, N>& strings)
{
QStringList result;
result.reserve(N);
for (const char* s : strings) {
result.append(QString::fromUtf8(s));
}
return result;
}
QString extractUnitFromResultString(const QString& resultString)
{
std::string str = resultString.toStdString();
auto lastSpace = str.find_last_of(' ');
if (lastSpace != std::string::npos && lastSpace < str.length() - 1) {
return QString::fromStdString(str.substr(lastSpace + 1));
}
return QString();
}
} // namespace
TaskMeasure::TaskMeasure()
@@ -85,7 +119,7 @@ TaskMeasure::TaskMeasure()
showDelta = new QCheckBox();
showDelta->setChecked(delta);
showDeltaLabel = new QLabel(tr("Show Delta:"));
showDeltaLabel = new QLabel(tr("Show Delta"));
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(showDelta, &QCheckBox::checkStateChanged, this, &TaskMeasure::showDeltaChanged);
#else
@@ -140,6 +174,11 @@ TaskMeasure::TaskMeasure()
// Connect dropdown's change signal to our onModeChange slot
connect(modeSwitch, qOverload<int>(&QComboBox::currentIndexChanged), this, &TaskMeasure::onModeChanged);
unitSwitch = new QComboBox();
unitSwitch->addItem("-");
connect(unitSwitch, qOverload<int>(&QComboBox::currentIndexChanged), this, &TaskMeasure::onUnitChanged);
// Result widget
valueResult = new QLineEdit();
valueResult->setReadOnly(true);
@@ -149,6 +188,7 @@ TaskMeasure::TaskMeasure()
QFormLayout* formLayout = new QFormLayout();
formLayout->setHorizontalSpacing(10);
formLayout->setVerticalSpacing(6);
// Note: How can the split between columns be kept in the middle?
// formLayout->setFieldGrowthPolicy(QFormLayout::FieldGrowthPolicy::ExpandingFieldsGrow);
formLayout->setFormAlignment(Qt::AlignCenter);
@@ -157,9 +197,22 @@ TaskMeasure::TaskMeasure()
settingsLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding));
settingsLayout->addWidget(mSettings);
formLayout->addRow(QLatin1String(), settingsLayout);
formLayout->addRow(tr("Mode:"), modeSwitch);
formLayout->addRow(showDeltaLabel, showDelta);
formLayout->addRow(tr("Result:"), valueResult);
formLayout->addRow(tr("Mode"), modeSwitch);
auto* deltaLayout = new QHBoxLayout();
deltaLayout->setContentsMargins(0, 0, 0, 0);
deltaLayout->setSpacing(8);
deltaLayout->addWidget(showDelta, 0, Qt::AlignVCenter | Qt::AlignLeft);
deltaLayout->addWidget(showDeltaLabel, 0, Qt::AlignVCenter | Qt::AlignLeft);
deltaLayout->addStretch(1);
auto* resultLayout = new QHBoxLayout();
resultLayout->setSpacing(8);
resultLayout->addWidget(valueResult, 65);
resultLayout->addWidget(unitSwitch, 30);
formLayout->addRow(tr("Result"), resultLayout);
formLayout->addRow(deltaLayout);
layout->addLayout(formLayout);
Content.emplace_back(taskbox);
@@ -307,13 +360,10 @@ void TaskMeasure::tryUpdate()
if (!measureType) {
// Note: If there's no valid measure type we might just restart the selection,
// however this requires enough coverage of measuretypes that we can access all of them
// std::tuple<std::string, std::string> sel = selection.back();
// clearSelection();
// addElement(measureModule.c_str(), get<0>(sel).c_str(), get<1>(sel).c_str());
QSignalBlocker unitSwitchBlocker(unitSwitch);
unitSwitch->clear();
unitSwitch->addItem(QLatin1String("-"));
mLastUnitSelection = QLatin1String("-");
// Reset measure object
if (!explicitMode) {
@@ -324,6 +374,8 @@ void TaskMeasure::tryUpdate()
return;
}
updateUnitDropdown(measureType);
// Update tool mode display
setModeSilent(measureType);
@@ -341,9 +393,9 @@ void TaskMeasure::tryUpdate()
// Fill measure object's properties from selection
_mMeasureObject->parseSelection(selection);
// Get result
valueResult->setText(_mMeasureObject->getResultString());
setUnitFromResultString();
updateResultWithUnit();
// Initialite the measurement's viewprovider
initViewObject(_mMeasureObject);
@@ -351,6 +403,101 @@ void TaskMeasure::tryUpdate()
_mMeasureObject->purgeTouched();
}
void TaskMeasure::updateUnitDropdown(const App::MeasureType* measureType)
{
const QString previousUnit = unitSwitch->currentText();
QStringList units;
if (measureType->identifier == "LENGTH" || measureType->identifier == "DISTANCE"
|| measureType->identifier == "DISTANCEFREE" || measureType->identifier == "RADIUS"
|| measureType->identifier == "DIAMETER" || measureType->identifier == "POSITION"
|| measureType->identifier == "CENTEROFMASS") {
units = toQStringList(lengthUnitLabels);
}
else if (measureType->identifier == "ANGLE") {
units = toQStringList(angleUnitLabels);
}
else if (measureType->identifier == "AREA") {
units = toQStringList(areaUnitLabels);
}
else {
units.clear();
}
QSignalBlocker unitSwitchBlocker(unitSwitch);
unitSwitch->clear();
if (!units.isEmpty()) {
unitSwitch->addItems(units);
// If unit from the same category was previously selected keep it
if (!previousUnit.isEmpty()) {
int unitIndex = unitSwitch->findText(previousUnit);
if (unitIndex >= 0) {
unitSwitch->setCurrentIndex(unitIndex);
}
}
}
}
void TaskMeasure::setUnitFromResultString()
{
if (!_mMeasureObject) {
return;
}
// Only set default unit if user hasn't made a selection yet
if (mLastUnitSelection != QLatin1String("-") && !mLastUnitSelection.isEmpty()) {
return;
}
QString resultString = _mMeasureObject->getResultString();
QString unitFromResult = extractUnitFromResultString(resultString);
if (unitFromResult.isEmpty()) {
return;
}
int unitIndex = unitSwitch->findText(unitFromResult);
if (unitIndex >= 0) {
QSignalBlocker unitSwitchBlocker(unitSwitch);
unitSwitch->setCurrentIndex(unitIndex);
mLastUnitSelection = unitFromResult;
}
}
void TaskMeasure::updateResultWithUnit()
{
if (!_mMeasureObject) {
return;
}
QString resultString = _mMeasureObject->getResultString();
QString currentUnit = unitSwitch->currentText();
if (currentUnit != QLatin1String("-") && !resultString.isEmpty()) {
Base::Quantity resultQty = Base::Quantity::parse(resultString.toStdString());
// Parse unit string like "1 mm" to get the target quantity
Base::Quantity targetUnit = Base::Quantity::parse(("1 " + currentUnit).toStdString());
double convertedValue = resultQty.getValueAs(targetUnit);
QString formattedValue;
// 4 decimal places, if between -1 and 1: 4 significant digits
if (std::abs(convertedValue) < 1.0 && convertedValue != 0.0) {
formattedValue = QString::number(convertedValue, 'g', 4);
}
else {
formattedValue = QString::number(convertedValue, 'f', 4);
}
QString formattedResult = formattedValue + " " + currentUnit;
valueResult->setText(formattedResult);
}
else {
valueResult->setText(resultString);
}
}
void TaskMeasure::initViewObject(Measure::MeasureBase* measure)
{
@@ -559,6 +706,18 @@ void TaskMeasure::onModeChanged(int index)
this->update();
}
void TaskMeasure::onUnitChanged(int index)
{
const QString currentUnit = unitSwitch->itemText(index);
const auto dash = QLatin1String("-");
if (currentUnit != mLastUnitSelection && (mLastUnitSelection != dash || currentUnit != dash)) {
updateResultWithUnit();
}
mLastUnitSelection = currentUnit;
}
void TaskMeasure::showDeltaChanged(int checkState)
{
delta = checkState == Qt::CheckState::Checked;

View File

@@ -75,6 +75,8 @@ public:
private:
void setupShortcuts(QWidget* parent);
void tryUpdate();
void updateUnitDropdown(const App::MeasureType* measureType);
void setUnitFromResultString();
void onSelectionChanged(const Gui::SelectionChanges& msg) override;
void onObjectDeleted(const App::DocumentObject& obj);
void saveMeasurement();
@@ -84,6 +86,7 @@ private:
QLineEdit* valueResult {nullptr};
QComboBox* modeSwitch {nullptr};
QComboBox* unitSwitch {nullptr};
QCheckBox* showDelta {nullptr};
QLabel* showDeltaLabel {nullptr};
QAction* autoSaveAction {nullptr};
@@ -94,6 +97,7 @@ private:
void removeObject();
void onModeChanged(int index);
void onUnitChanged(int index);
void showDeltaChanged(int checkState);
void autoSaveChanged(bool checked);
void newMeasurementBehaviourChanged(bool checked);
@@ -104,6 +108,7 @@ private:
void ensureGroup(Measure::MeasureBase* measurement);
void setDeltaPossible(bool possible);
void initViewObject(Measure::MeasureBase* measure);
void updateResultWithUnit();
// Stores if the mode is explicitly set by the user or implicitly through the selection
bool explicitMode = false;
@@ -111,6 +116,7 @@ private:
// Stores if delta measures shall be shown
bool delta = true;
bool mAutoSave = false;
QString mLastUnitSelection = QLatin1String("-");
};
} // namespace MeasureGui