From f8cc0b0c5150e23c02f9c667054b35e938e00855 Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Tue, 18 Aug 2026 08:38:44 +0200 Subject: [PATCH 1/4] Add configurable scatterplot z ordering Introduce a dedicated `ZOrderingAction` for choosing point depth order by insertion order, dimension, or randomized mode, and expose it in the settings toolbar/menu. The scatterplot plugin and widget now support data-driven z-order scalars, update z ordering when position data changes, and migrate older saved settings that stored randomized depth under miscellaneous options. --- CMakeLists.txt | 2 + src/MiscellaneousAction.cpp | 17 +--- src/MiscellaneousAction.h | 5 +- src/ScatterplotPlugin.cpp | 13 +++ src/ScatterplotPlugin.h | 3 +- src/ScatterplotWidget.cpp | 23 +++++- src/ScatterplotWidget.h | 18 ++-- src/SettingsAction.cpp | 20 +++++ src/SettingsAction.h | 3 + src/ZOrderingAction.cpp | 161 ++++++++++++++++++++++++++++++++++++ src/ZOrderingAction.h | 52 ++++++++++++ 11 files changed, 289 insertions(+), 28 deletions(-) create mode 100644 src/ZOrderingAction.cpp create mode 100644 src/ZOrderingAction.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 39453a5..28e0c25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,8 @@ set(Actions src/SettingsAction.cpp src/SubsetAction.h src/SubsetAction.cpp + src/ZOrderingAction.h + src/ZOrderingAction.cpp src/ExportAction.h src/ExportAction.cpp src/DatasetsAction.h diff --git a/src/MiscellaneousAction.cpp b/src/MiscellaneousAction.cpp index 3222798..5747a22 100644 --- a/src/MiscellaneousAction.cpp +++ b/src/MiscellaneousAction.cpp @@ -10,15 +10,13 @@ const QColor MiscellaneousAction::DEFAULT_BACKGROUND_COLOR = qRgb(255, 255, 255) MiscellaneousAction::MiscellaneousAction(QObject* parent, const QString& title) : VerticalGroupAction(parent, title), _scatterplotPlugin(dynamic_cast(parent->parent())), - _backgroundColorAction(this, "Background color"), - _randomizedDepthAction(this, "Randomized depth", true) + _backgroundColorAction(this, "Background color") { setIconByName("cog"); setLabelSizingType(LabelSizingType::Auto); setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); addAction(&_backgroundColorAction); - addAction(&_randomizedDepthAction); _backgroundColorAction.setColor(DEFAULT_BACKGROUND_COLOR); @@ -32,15 +30,6 @@ MiscellaneousAction::MiscellaneousAction(QObject* parent, const QString& title) updateBackgroundColor(); - const auto updateRandomizedDepth = [this]() -> void { - _scatterplotPlugin->getScatterplotWidget().setRandomizedDepthEnabled(_randomizedDepthAction.isChecked()); - }; - - connect(&_randomizedDepthAction, &ToggleAction::toggled, this, [this, updateRandomizedDepth](bool toggled) { - updateRandomizedDepth(); - }); - - updateRandomizedDepth(); } QMenu* MiscellaneousAction::getContextMenu() @@ -85,7 +74,6 @@ void MiscellaneousAction::fromVariantMap(const QVariantMap& variantMap) GroupAction::fromVariantMap(variantMap); _backgroundColorAction.fromParentVariantMap(variantMap); - _randomizedDepthAction.fromParentVariantMap(variantMap); } QVariantMap MiscellaneousAction::toVariantMap() const @@ -93,7 +81,6 @@ QVariantMap MiscellaneousAction::toVariantMap() const auto variantMap = GroupAction::toVariantMap(); _backgroundColorAction.insertIntoVariantMap(variantMap); - _randomizedDepthAction.insertIntoVariantMap(variantMap); return variantMap; -} \ No newline at end of file +} diff --git a/src/MiscellaneousAction.h b/src/MiscellaneousAction.h index 59189a4..2c7dfb8 100644 --- a/src/MiscellaneousAction.h +++ b/src/MiscellaneousAction.h @@ -2,7 +2,6 @@ #include #include -#include using namespace mv::gui; @@ -66,12 +65,10 @@ class MiscellaneousAction : public VerticalGroupAction public: // Action getters ColorAction& getBackgroundColorAction() { return _backgroundColorAction; } - ToggleAction& getRandomizedDepthAction() { return _randomizedDepthAction; } private: ScatterplotPlugin* _scatterplotPlugin; /** Pointer to scatter plot plugin */ ColorAction _backgroundColorAction; /** Color action for setting the background color action */ - ToggleAction _randomizedDepthAction; /** whether the z-order of each point is to be randomized or not */ static const QColor DEFAULT_BACKGROUND_COLOR; @@ -80,4 +77,4 @@ class MiscellaneousAction : public VerticalGroupAction Q_DECLARE_METATYPE(MiscellaneousAction) -inline const auto miscellaneousActionMetaTypeId = qRegisterMetaType("MiscellaneousAction"); \ No newline at end of file +inline const auto miscellaneousActionMetaTypeId = qRegisterMetaType("MiscellaneousAction"); diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 2f18488..3dddf06 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -87,6 +87,7 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : _primaryToolbarAction->addAction(&_settingsAction->getDatasetsAction()); _primaryToolbarAction->addAction(&_settingsAction->getRenderModeAction(), 3, GroupAction::Horizontal); _primaryToolbarAction->addAction(&_settingsAction->getPositionAction(), 1, GroupAction::Horizontal); + _primaryToolbarAction->addAction(&_settingsAction->getZOrderingAction(), 1, GroupAction::Horizontal); _primaryToolbarAction->addAction(&_settingsAction->getPlotAction(), 2, GroupAction::Horizontal); _primaryToolbarAction->addAction(&_settingsAction->getColoringAction()); _primaryToolbarAction->addAction(&_settingsAction->getSubsetAction()); @@ -957,6 +958,16 @@ ScatterplotWidget& ScatterplotPlugin::getScatterplotWidget() return *_scatterPlotWidget; } +void ScatterplotPlugin::setZOrderDimension(const std::int32_t& dimensionIndex) +{ + std::vector zOrderScalars; + + if (_positionDataset.isValid() && dimensionIndex >= 0 && dimensionIndex < static_cast(_positionDataset->getNumDimensions())) + _positionDataset->extractDataForDimension(zOrderScalars, dimensionIndex); + + _scatterPlotWidget->setZOrderScalars(zOrderScalars); +} + void ScatterplotPlugin::updateData() { // Check if the scatter plot is initialized, if not, don't do anything @@ -989,6 +1000,7 @@ void ScatterplotPlugin::updateData() // Pass the 2D points to the scatter plot widget _scatterPlotWidget->setData(&_positions); + _settingsAction->getZOrderingAction().updateScatterplotWidget(); updateSelection(); } @@ -996,6 +1008,7 @@ void ScatterplotPlugin::updateData() _numPoints = 0; _positions.clear(); _scatterPlotWidget->setData(&_positions); + _settingsAction->getZOrderingAction().updateScatterplotWidget(); } } diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index ef32af8..718fd87 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -48,6 +48,7 @@ class ScatterplotPlugin : public ViewPlugin public: // Dimension picking void setXDimension(const std::int32_t& dimensionIndex); void setYDimension(const std::int32_t& dimensionIndex); + void setZOrderDimension(const std::int32_t& dimensionIndex); protected: // Data loading @@ -184,4 +185,4 @@ class ScatterplotPluginFactory : public ViewPluginFactory * @return URL of the GitHub repository (or readme markdown URL if set) */ QUrl getRepositoryUrl() const override; -}; \ No newline at end of file +}; diff --git a/src/ScatterplotWidget.cpp b/src/ScatterplotWidget.cpp index 837ed7c..f53b1e9 100644 --- a/src/ScatterplotWidget.cpp +++ b/src/ScatterplotWidget.cpp @@ -613,6 +613,25 @@ void ScatterplotWidget::setRandomizedDepthEnabled(bool randomizedDepth) update(); } +PointZOrderMode ScatterplotWidget::getZOrderMode() const +{ + return _pointRenderer.getZOrderMode(); +} + +void ScatterplotWidget::setZOrderMode(PointZOrderMode zOrderMode) +{ + _pointRenderer.setZOrderMode(zOrderMode); + + update(); +} + +void ScatterplotWidget::setZOrderScalars(const std::vector& zOrderScalars) +{ + _pointRenderer.setZOrderChannelScalars(zOrderScalars); + + update(); +} + bool ScatterplotWidget::getRandomizedDepthEnabled() const { return _pointRenderer.getRandomizedDepthEnabled(); @@ -679,8 +698,10 @@ void ScatterplotWidget::paintGL() // Reset the blending function glEnable(GL_BLEND); - if (getRandomizedDepthEnabled()) + if (_renderMode == SCATTERPLOT && getZOrderMode() != PointZOrderMode::InsertionOrder) glEnable(GL_DEPTH_TEST); + else + glDisable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); diff --git a/src/ScatterplotWidget.h b/src/ScatterplotWidget.h index 4441bd6..fee78a5 100644 --- a/src/ScatterplotWidget.h +++ b/src/ScatterplotWidget.h @@ -104,6 +104,13 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C */ void setPointOpacityScalars(const std::vector& pointOpacityScalars); + /** Get/set how point depth is determined. */ + PointZOrderMode getZOrderMode() const; + void setZOrderMode(PointZOrderMode zOrderMode); + + /** Set the scalar channel used by data-driven z ordering. */ + void setZOrderScalars(const std::vector& zOrderScalars); + void setScalarEffect(PointEffect effect); void setPointScaling(PointScaling scalingMode); @@ -203,16 +210,13 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C /** * Set whether the selection outline halo is enabled or not - * @param randomizedDepth Boolean determining whether the selection outline halo is enabled or not - */ - void setRandomizedDepthEnabled(bool randomizedDepth); - - /** - * Set whether the z-order of each point is to be randomized or not - * @param selectionOutlineHaloEnabled Boolean determining whether the z-order of each point is to be randomized or not + * @param selectionOutlineHaloEnabled Boolean determining whether the selection outline halo is enabled or not */ void setSelectionOutlineHaloEnabled(bool selectionOutlineHaloEnabled); + /** Compatibility wrapper for selecting randomized or insertion-order depth. */ + void setRandomizedDepthEnabled(bool randomizedDepth); + /** * Get whether the z-order of each point is to be randomized or not * @return Boolean determining whether the z-order of each point is to be randomized or not diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index f0739dc..d8aed0c 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -15,6 +15,7 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : _scatterplotPlugin(dynamic_cast(parent)), _renderModeAction(this, "Render Mode"), _positionAction(this, "Position"), + _zOrderingAction(this, "Z ordering"), _plotAction(this, "Plot"), _coloringAction(this, "Coloring"), _subsetAction(this, "Subset"), @@ -27,6 +28,7 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : setConnectionPermissionsToForceNone(); _renderModeAction.initialize(_scatterplotPlugin); + _zOrderingAction.initialize(_scatterplotPlugin); _plotAction.initialize(_scatterplotPlugin); _subsetAction.initialize(_scatterplotPlugin); _selectionAction.initialize(_scatterplotPlugin); @@ -38,6 +40,7 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : _plotAction.setEnabled(enabled); _positionAction.setEnabled(enabled); _coloringAction.setEnabled(enabled); + _zOrderingAction.setEnabled(enabled); }; updateEnabled(); @@ -53,6 +56,7 @@ QMenu* SettingsAction::getContextMenu() menu->addMenu(_plotAction.getContextMenu()); menu->addSeparator(); menu->addMenu(_positionAction.getContextMenu()); + menu->addMenu(_zOrderingAction.getContextMenu()); menu->addMenu(_coloringAction.getContextMenu()); menu->addSeparator(); menu->addMenu(_subsetAction.getContextMenu()); @@ -66,9 +70,12 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) { WidgetAction::fromVariantMap(variantMap); + const auto containsZOrderingSettings = variantMap.contains("Z ordering"); + _datasetsAction.fromParentVariantMap(variantMap); _plotAction.fromParentVariantMap(variantMap); _positionAction.fromParentVariantMap(variantMap); + _zOrderingAction.fromParentVariantMap(variantMap, true); _coloringAction.fromParentVariantMap(variantMap); _subsetAction.fromParentVariantMap(variantMap, true); _clusteringAction.fromParentVariantMap(variantMap, true); @@ -76,6 +83,18 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) _selectionAction.fromParentVariantMap(variantMap); _miscellaneousAction.fromParentVariantMap(variantMap); + // Migrate projects saved before z ordering became a dedicated action. + if (!containsZOrderingSettings) { + const auto miscellaneousMap = variantMap.value("Miscellaneous").toMap(); + const auto randomizedDepthMap = miscellaneousMap.value("Randomized depth").toMap(); + + if (!randomizedDepthMap.isEmpty()) { + const auto mode = randomizedDepthMap.value("Value").toBool() ? ZOrderingAction::Mode::Randomized : ZOrderingAction::Mode::InsertionOrder; + + _zOrderingAction.getModeAction().setCurrentIndex(static_cast(mode)); + } + } + if (variantMap.contains("PointRendererNavigation")) _scatterplotPlugin->getScatterplotWidget().getPointRendererNavigator().getNavigationAction().fromVariantMap(variantMap["PointRendererNavigation"].toMap()); @@ -91,6 +110,7 @@ QVariantMap SettingsAction::toVariantMap() const _renderModeAction.insertIntoVariantMap(variantMap); _plotAction.insertIntoVariantMap(variantMap); _positionAction.insertIntoVariantMap(variantMap); + _zOrderingAction.insertIntoVariantMap(variantMap); _coloringAction.insertIntoVariantMap(variantMap); _subsetAction.insertIntoVariantMap(variantMap); _clusteringAction.insertIntoVariantMap(variantMap); diff --git a/src/SettingsAction.h b/src/SettingsAction.h index a6b0ba9..362244c 100644 --- a/src/SettingsAction.h +++ b/src/SettingsAction.h @@ -12,6 +12,7 @@ #include "RenderModeAction.h" #include "SelectionAction.h" #include "SubsetAction.h" +#include "ZOrderingAction.h" using namespace mv::gui; @@ -59,6 +60,7 @@ class SettingsAction : public GroupAction RenderModeAction& getRenderModeAction() { return _renderModeAction; } PositionAction& getPositionAction() { return _positionAction; } + ZOrderingAction& getZOrderingAction() { return _zOrderingAction; } PlotAction& getPlotAction() { return _plotAction; } ColoringAction& getColoringAction() { return _coloringAction; } SubsetAction& getSubsetAction() { return _subsetAction; } @@ -72,6 +74,7 @@ class SettingsAction : public GroupAction ScatterplotPlugin* _scatterplotPlugin; /** Pointer to scatter plot plugin */ RenderModeAction _renderModeAction; /** Action for configuring render mode */ PositionAction _positionAction; /** Action for configuring point positions */ + ZOrderingAction _zOrderingAction; /** Action for configuring point z ordering */ PlotAction _plotAction; /** Action for configuring plot settings */ ColoringAction _coloringAction; /** Action for configuring point coloring */ SubsetAction _subsetAction; /** Action for creating subset(s) */ diff --git a/src/ZOrderingAction.cpp b/src/ZOrderingAction.cpp new file mode 100644 index 0000000..ff28bfc --- /dev/null +++ b/src/ZOrderingAction.cpp @@ -0,0 +1,161 @@ +#include "ZOrderingAction.h" + +#include "ScatterplotPlugin.h" +#include "ScatterplotWidget.h" + +#include + +ZOrderingAction::ZOrderingAction(QObject* parent, const QString& title) : + VerticalGroupAction(parent, title), + _modeAction(this, "Mode", { "Insertion order", "Dimension", "Randomized" }), + _dimensionPickerAction(this, "Dimension") +{ + setIconByName("layer-group"); + setLabelSizingType(LabelSizingType::Auto); + setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + + addAction(&_modeAction, OptionAction::HorizontalButtons); + addAction(&_dimensionPickerAction); + + _modeAction.setToolTip("Choose how overlapping points are ordered"); + _dimensionPickerAction.setToolTip("Dimension whose numerical values determine point depth"); + _dimensionPickerAction.setEnabled(false); +} + +void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) +{ + Q_ASSERT(scatterplotPlugin != nullptr); + + if (scatterplotPlugin == nullptr) + return; + + _scatterplotPlugin = scatterplotPlugin; + + const auto updateDataset = [this]() { + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + + if (!positionDataset.isValid()) { + _dimensionPickerAction.setPointsDataset(Dataset()); + updateScatterplotWidget(); + return; + } + + auto dimensionIndex = static_cast(_dimensionPickerAction.getCurrentDimensionIndex()); + const auto numberOfDimensions = static_cast(positionDataset->getNumDimensions()); + + if (dimensionIndex < 0 || dimensionIndex >= numberOfDimensions) + dimensionIndex = 0; + + _dimensionPickerAction.setPointsDataset(positionDataset); + _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + updateScatterplotWidget(); + }; + + connect(&_modeAction, &OptionAction::currentIndexChanged, this, [this]() { + updateScatterplotWidget(); + }); + + connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this]() { + updateScatterplotWidget(); + }); + + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataDimensionsChanged, this, updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataChanged, this, [this]() { + updateScatterplotWidget(); + }); + + _modeAction.setCurrentIndex(static_cast(Mode::InsertionOrder)); + updateDataset(); +} + +void ZOrderingAction::updateScatterplotWidget() +{ + if (_scatterplotPlugin == nullptr) + return; + + const auto mode = static_cast(_modeAction.getCurrentIndex()); + const auto hasDataset = _scatterplotPlugin->getPositionDataset().isValid(); + + setEnabled(hasDataset); + + _dimensionPickerAction.setEnabled(hasDataset && mode == Mode::Dimension); + + switch (mode) { + case Mode::InsertionOrder: + _scatterplotPlugin->getScatterplotWidget().setZOrderMode(PointZOrderMode::InsertionOrder); + break; + + case Mode::Dimension: + _scatterplotPlugin->getScatterplotWidget().setZOrderMode(PointZOrderMode::Dimension); + break; + + case Mode::Randomized: + _scatterplotPlugin->getScatterplotWidget().setZOrderMode(PointZOrderMode::Randomized); + break; + } + + if (mode == Mode::Dimension && hasDataset) + _scatterplotPlugin->setZOrderDimension(_dimensionPickerAction.getCurrentDimensionIndex()); + else if (!hasDataset) + _scatterplotPlugin->setZOrderDimension(-1); +} + +QMenu* ZOrderingAction::getContextMenu(QWidget* parent) +{ + auto menu = new QMenu("Z ordering", parent); + + menu->addAction(&_modeAction); + menu->addAction(&_dimensionPickerAction); + + return menu; +} + +void ZOrderingAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) +{ + auto publicZOrderingAction = dynamic_cast(publicAction); + + Q_ASSERT(publicZOrderingAction != nullptr); + + if (publicZOrderingAction == nullptr) + return; + + if (recursive) { + actions().connectPrivateActionToPublicAction(&_modeAction, &publicZOrderingAction->getModeAction(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionPickerAction, &publicZOrderingAction->getDimensionPickerAction(), recursive); + } + + GroupAction::connectToPublicAction(publicAction, recursive); +} + +void ZOrderingAction::disconnectFromPublicAction(bool recursive) +{ + if (!isConnected()) + return; + + if (recursive) { + actions().disconnectPrivateActionFromPublicAction(&_modeAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); + } + + GroupAction::disconnectFromPublicAction(recursive); +} + +void ZOrderingAction::fromVariantMap(const QVariantMap& variantMap) +{ + GroupAction::fromVariantMap(variantMap); + + _modeAction.fromParentVariantMap(variantMap); + _dimensionPickerAction.fromParentVariantMap(variantMap); + updateScatterplotWidget(); +} + +QVariantMap ZOrderingAction::toVariantMap() const +{ + auto variantMap = GroupAction::toVariantMap(); + + _modeAction.insertIntoVariantMap(variantMap); + _dimensionPickerAction.insertIntoVariantMap(variantMap); + + return variantMap; +} diff --git a/src/ZOrderingAction.h b/src/ZOrderingAction.h new file mode 100644 index 0000000..a890689 --- /dev/null +++ b/src/ZOrderingAction.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include + +using namespace mv::gui; + +class QMenu; +class ScatterplotPlugin; + +/** Action for choosing how overlapping points are ordered along the z-axis. */ +class ZOrderingAction : public VerticalGroupAction +{ +public: + enum class Mode { + InsertionOrder, + Dimension, + Randomized + }; + + Q_INVOKABLE ZOrderingAction(QObject* parent, const QString& title); + + void initialize(ScatterplotPlugin* scatterplotPlugin); + void updateScatterplotWidget(); + + QMenu* getContextMenu(QWidget* parent = nullptr) override; + +protected: // Linking + void connectToPublicAction(WidgetAction* publicAction, bool recursive) override; + void disconnectFromPublicAction(bool recursive) override; + +public: // Serialization + void fromVariantMap(const QVariantMap& variantMap) override; + QVariantMap toVariantMap() const override; + +public: // Action getters + OptionAction& getModeAction() { return _modeAction; } + DimensionPickerAction& getDimensionPickerAction() { return _dimensionPickerAction; } + +private: + ScatterplotPlugin* _scatterplotPlugin = nullptr; + OptionAction _modeAction; + DimensionPickerAction _dimensionPickerAction; + + friend class mv::AbstractActionsManager; +}; + +Q_DECLARE_METATYPE(ZOrderingAction) + +inline const auto zOrderingActionMetaTypeId = qRegisterMetaType("ZOrderingAction"); From 4c12bb6febcd93695b2aa855066508644dedca2e Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Tue, 18 Aug 2026 09:50:00 +0200 Subject: [PATCH 2/4] Restrict selection by Z-order threshold Add Z-order selection filtering with a configurable minimum value and enable/disable toggle, wired through `ZOrderingAction` and mirrored in `SelectionAction`. Selection operations now respect excluded points (interactive select, sample, select all, invert), widget highlights are masked for excluded indices, and the HUD reports effective selected vs selectable point counts. --- src/ScatterplotPlugin.cpp | 110 +++++++++++++++++++++++++++---- src/ScatterplotPlugin.h | 5 +- src/ScatterplotWidget.cpp | 79 +++++++++++++++++++++- src/ScatterplotWidget.h | 17 +++++ src/SelectionAction.cpp | 60 +++++++++++++++-- src/SelectionAction.h | 6 +- src/ZOrderingAction.cpp | 135 ++++++++++++++++++++++++++++++++++++-- src/ZOrderingAction.h | 10 +++ 8 files changed, 398 insertions(+), 24 deletions(-) diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 3dddf06..4c246a2 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -458,6 +458,78 @@ void ScatterplotPlugin::createSubset(const bool& fromSourceData /*= false*/, con subset->getDataHierarchyItem().select(); } +void ScatterplotPlugin::selectAllEligiblePoints() +{ + if (!_positionDataset.isValid()) + return; + + std::vector globalIndices; + _positionDataset->getGlobalIndices(globalIndices); + + std::vector eligibleIndices; + eligibleIndices.reserve(globalIndices.size()); + + for (std::uint32_t localIndex = 0; localIndex < globalIndices.size(); ++localIndex) { + if (!_scatterPlotWidget->isSelectionExcluded(localIndex)) + eligibleIndices.push_back(globalIndices[localIndex]); + } + + _positionDataset->setSelectionIndices(eligibleIndices); + events().notifyDatasetDataSelectionChanged(_positionDataset->getSourceDataset()); +} + +void ScatterplotPlugin::invertEligiblePointSelection() +{ + if (!_positionDataset.isValid()) + return; + + const auto selection = _positionDataset->getSelection(); + + std::vector selected; + _positionDataset->selectedLocalIndices(selection->indices, selected); + + std::vector globalIndices; + _positionDataset->getGlobalIndices(globalIndices); + + std::vector invertedIndices; + invertedIndices.reserve(globalIndices.size()); + + for (std::uint32_t localIndex = 0; localIndex < globalIndices.size(); ++localIndex) { + if (!_scatterPlotWidget->isSelectionExcluded(localIndex) && (localIndex >= selected.size() || !selected[localIndex])) + invertedIndices.push_back(globalIndices[localIndex]); + } + + _positionDataset->setSelectionIndices(invertedIndices); + events().notifyDatasetDataSelectionChanged(_positionDataset->getSourceDataset()); +} + +void ScatterplotPlugin::filterSelectionExcludedIndices(std::vector& globalIndices) const +{ + if (!_positionDataset.isValid()) { + globalIndices.clear(); + return; + } + + std::vector selected; + _positionDataset->selectedLocalIndices(globalIndices, selected); + + std::vector localGlobalIndices; + _positionDataset->getGlobalIndices(localGlobalIndices); + + globalIndices.clear(); + globalIndices.reserve(localGlobalIndices.size()); + + for (std::uint32_t localIndex = 0; localIndex < localGlobalIndices.size(); ++localIndex) { + if (localIndex < selected.size() && selected[localIndex] && !_scatterPlotWidget->isSelectionExcluded(localIndex)) + globalIndices.push_back(localGlobalIndices[localIndex]); + } +} + +void ScatterplotPlugin::refreshSelection() +{ + updateSelection(); +} + void ScatterplotPlugin::selectPoints() { if (getSettingsAction().getSelectionAction().getFreezeSelectionAction().isChecked()) @@ -495,6 +567,9 @@ void ScatterplotPlugin::selectPoints() // Go over all points in the dataset to see if they are selected for (std::uint32_t localPointIndex = 0; localPointIndex < _positions.size(); localPointIndex++) { + if (_scatterPlotWidget->isSelectionExcluded(localPointIndex)) + continue; + const auto& point = _positions[localPointIndex]; // Compute the offset of the point in the world space @@ -569,6 +644,8 @@ void ScatterplotPlugin::selectPoints() } } + filterSelectionExcludedIndices(targetSelectionIndices); + auto& navigationAction = navigator.getNavigationAction(); navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && navigationAction.isNavigationActive()); @@ -609,6 +686,9 @@ void ScatterplotPlugin::samplePoints() // Go over all points in the dataset to see if they should be sampled for (std::uint32_t localPointIndex = 0; localPointIndex < _positions.size(); localPointIndex++) { + if (_scatterPlotWidget->isSelectionExcluded(localPointIndex)) + continue; + // Compute the offset of the point in the world space const auto pointOffsetWorld = QPointF(_positions[localPointIndex].x - zoomRectangleWorld.left(), _positions[localPointIndex].y - zoomRectangleWorld.top()); @@ -958,16 +1038,6 @@ ScatterplotWidget& ScatterplotPlugin::getScatterplotWidget() return *_scatterPlotWidget; } -void ScatterplotPlugin::setZOrderDimension(const std::int32_t& dimensionIndex) -{ - std::vector zOrderScalars; - - if (_positionDataset.isValid() && dimensionIndex >= 0 && dimensionIndex < static_cast(_positionDataset->getNumDimensions())) - _positionDataset->extractDataForDimension(zOrderScalars, dimensionIndex); - - _scatterPlotWidget->setZOrderScalars(zOrderScalars); -} - void ScatterplotPlugin::updateData() { // Check if the scatter plot is initialized, if not, don't do anything @@ -1042,14 +1112,16 @@ void ScatterplotPlugin::updateSelection() sampledPoints.reserve(_positions.size()); - for (auto selectionIndex : selection->indices) - sampledPoints.push_back(selectionIndex); + for (std::uint32_t localIndex = 0; localIndex < selected.size(); ++localIndex) { + if (selected[localIndex] && !_scatterPlotWidget->isSelectionExcluded(localIndex)) + sampledPoints.push_back(localIndex); + } std::int32_t numberOfPoints = 0; QVariantList localPointIndices, globalPointIndices; - const auto numberOfSelectedPoints = selection->indices.size(); + const auto numberOfSelectedPoints = sampledPoints.size(); localPointIndices.reserve(static_cast(numberOfSelectedPoints)); globalPointIndices.reserve(static_cast(numberOfSelectedPoints)); @@ -1085,6 +1157,8 @@ void ScatterplotPlugin::updateSelection() { "RenderMode", _settingsAction->getRenderModeAction().getCurrentText() } }); } + + updateHeadsUpDisplay(); } void ScatterplotPlugin::updateHeadsUpDisplay() @@ -1114,6 +1188,16 @@ void ScatterplotPlugin::updateHeadsUpDisplay() //qDebug() << "ScatterplotPlugin::updateHeadsUpDisplay: point size dataset: " << pointPlotAction.getSizeAction().getCurrentDataset().isValid() << ", opacity dataset: " << pointPlotAction.getOpacityAction().getCurrentDataset().isValid(); addMetaDataToHeadsUpDisplay("Size", pointPlotAction.getSizeAction().getCurrentDataset(), datasetsItem); addMetaDataToHeadsUpDisplay("Opacity", pointPlotAction.getOpacityAction().getCurrentDataset(), datasetsItem); + + const auto selectionItem = getHeadsUpDisplayAction().addHeadsUpDisplayItem("Selection", "", ""); + const auto numberOfSelectedPoints = _scatterPlotWidget->getNumberOfEffectivelySelectedPoints(); + const auto numberOfSelectablePoints = _scatterPlotWidget->getNumberOfSelectablePoints(); + + getHeadsUpDisplayAction().addHeadsUpDisplayItem( + "Selected:", + QString("%1 of %2 selectable points").arg(numberOfSelectedPoints).arg(numberOfSelectablePoints), + "", + selectionItem); } else { getHeadsUpDisplayAction().addHeadsUpDisplayItem("No datasets loaded", "", ""); } diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index 718fd87..12a5338 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -44,11 +44,13 @@ class ScatterplotPlugin : public ViewPlugin public: void createSubset(const bool& fromSourceData = false, const QString& name = ""); + void selectAllEligiblePoints(); + void invertEligiblePointSelection(); + void refreshSelection(); public: // Dimension picking void setXDimension(const std::int32_t& dimensionIndex); void setYDimension(const std::int32_t& dimensionIndex); - void setZOrderDimension(const std::int32_t& dimensionIndex); protected: // Data loading @@ -112,6 +114,7 @@ class ScatterplotPlugin : public ViewPlugin void updateData(); void updateSelection(); void updateHeadsUpDisplayTextColor(); + void filterSelectionExcludedIndices(std::vector& globalIndices) const; public: diff --git a/src/ScatterplotWidget.cpp b/src/ScatterplotWidget.cpp index f53b1e9..8535092 100644 --- a/src/ScatterplotWidget.cpp +++ b/src/ScatterplotWidget.cpp @@ -286,6 +286,10 @@ void ScatterplotWidget::setData(const std::vector* points) _pointRenderer.setData(*points); _densityRenderer.setData(points); + _selectionExcludedIndices.clear(); + _selectionExclusionMask.assign(points->size(), 0); + updateEffectiveHighlights(); + switch (_renderMode) { case ScatterplotWidget::SCATTERPLOT: @@ -320,11 +324,84 @@ void ScatterplotWidget::setBackgroundColor(QColor color) void ScatterplotWidget::setHighlights(const std::vector& highlights, const std::int32_t& numSelectedPoints) { - _pointRenderer.setHighlights(highlights, numSelectedPoints); + Q_UNUSED(numSelectedPoints); + + _selectionHighlights = highlights; + updateEffectiveHighlights(); update(); } +void ScatterplotWidget::setSelectionExcludedIndices(const std::vector& excludedIndices) +{ + _selectionExcludedIndices.clear(); + _selectionExcludedIndices.reserve(excludedIndices.size()); + _selectionExclusionMask.assign(_pointRenderer.getGpuPoints().getPositions().size(), 0); + + for (const auto index : excludedIndices) { + if (index < _selectionExclusionMask.size() && _selectionExclusionMask[index] == 0) { + _selectionExcludedIndices.push_back(index); + _selectionExclusionMask[index] = 1; + } + } + + updateEffectiveHighlights(); + + update(); +} + +void ScatterplotWidget::clearSelectionExcludedIndices() +{ + _selectionExcludedIndices.clear(); + _selectionExclusionMask.assign(_pointRenderer.getGpuPoints().getPositions().size(), 0); + updateEffectiveHighlights(); + + update(); +} + +const std::vector& ScatterplotWidget::getSelectionExcludedIndices() const +{ + return _selectionExcludedIndices; +} + +bool ScatterplotWidget::isSelectionExcluded(std::uint32_t localPointIndex) const +{ + return localPointIndex < _selectionExclusionMask.size() && _selectionExclusionMask[localPointIndex] != 0; +} + +std::uint32_t ScatterplotWidget::getNumberOfSelectablePoints() const +{ + return static_cast(_selectionExclusionMask.size() - _selectionExcludedIndices.size()); +} + +std::uint32_t ScatterplotWidget::getNumberOfEffectivelySelectedPoints() const +{ + std::uint32_t numberOfSelectedPoints = 0; + + for (std::uint32_t index = 0; index < _selectionHighlights.size(); ++index) { + if (_selectionHighlights[index] != 0 && !isSelectionExcluded(index)) + ++numberOfSelectedPoints; + } + + return numberOfSelectedPoints; +} + +void ScatterplotWidget::updateEffectiveHighlights() +{ + auto effectiveHighlights = _selectionHighlights; + std::int32_t numberOfEffectiveHighlights = 0; + + for (std::size_t index = 0; index < effectiveHighlights.size(); ++index) { + if (isSelectionExcluded(static_cast(index))) + effectiveHighlights[index] = -1; + + if (effectiveHighlights[index] > 0) + ++numberOfEffectiveHighlights; + } + + _pointRenderer.setHighlights(effectiveHighlights, numberOfEffectiveHighlights); +} + void ScatterplotWidget::setScalars(const std::vector& scalars) { _pointRenderer.setColorChannelScalars(scalars); diff --git a/src/ScatterplotWidget.h b/src/ScatterplotWidget.h index fee78a5..818c22e 100644 --- a/src/ScatterplotWidget.h +++ b/src/ScatterplotWidget.h @@ -80,6 +80,17 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C void setHighlights(const std::vector& highlights, const std::int32_t& numSelectedPoints); void setScalars(const std::vector& scalars); + /** + * Exclude local point indices from selection in this scatterplot. + * Exclusions are applied to both interactive and externally supplied selections. + */ + void setSelectionExcludedIndices(const std::vector& excludedIndices); + void clearSelectionExcludedIndices(); + const std::vector& getSelectionExcludedIndices() const; + bool isSelectionExcluded(std::uint32_t localPointIndex) const; + std::uint32_t getNumberOfSelectablePoints() const; + std::uint32_t getNumberOfEffectivelySelectedPoints() const; + /** Set the second color scalar channel (used for 2D and RGB coloring) */ void setScalars2(const std::vector& scalars); @@ -299,6 +310,9 @@ public slots: private slots: void updatePixelRatio(); +private: + void updateEffectiveHighlights(); + protected: PointRenderer _pointRenderer; /** For rendering point data as points */ DensityRenderer _densityRenderer; /** For rendering point data as a density plot */ @@ -315,6 +329,9 @@ private slots: PixelSelectionTool _samplerPixelSelectionTool; /** 2D pixel selection tool */ float _pixelRatio; /** Current pixel ratio */ bool _weightDensity; /** Use point scalar sizes to weight density */ + std::vector _selectionHighlights; /** Selection highlights before local exclusions */ + std::vector _selectionExcludedIndices; /** Local point indices excluded from selection */ + std::vector _selectionExclusionMask; /** Mask of points excluded from selection */ mv::plugin::ViewPlugin* _parentPlugin = nullptr; diff --git a/src/SelectionAction.cpp b/src/SelectionAction.cpp index 0135b40..8af81be 100644 --- a/src/SelectionAction.cpp +++ b/src/SelectionAction.cpp @@ -15,7 +15,9 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : _outlineScaleAction(this, "Scale", 100.0f, 500.0f, 200.0f, 1), _outlineOpacityAction(this, "Opacity", 0.0f, 100.0f, 100.0f, 1), _outlineHaloEnabledAction(this, "Halo"), - _freezeSelectionAction(this, "Freeze selection") + _freezeSelectionAction(this, "Freeze selection"), + _zOrderSelectionThresholdEnabledAction(this, "Restrict selection by Z order", false), + _zOrderSelectionThresholdAction(this, "Minimum selectable value", 0.0f, 1.0f, 0.0f, 3) { setIconByName("mouse-pointer"); @@ -36,6 +38,8 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : addAction(&getOutlineOpacityAction()); addAction(&getOutlineHaloEnabledAction()); addAction(&getFreezeSelectionAction()); + addAction(&_zOrderSelectionThresholdEnabledAction); + addAction(&_zOrderSelectionThresholdAction); _pixelSelectionAction.getOverlayColorAction().setText("Color"); @@ -43,6 +47,8 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : _outlineScaleAction.setSuffix("%"); _outlineOpacityAction.setSuffix("%"); + _zOrderSelectionThresholdEnabledAction.setToolTip("Mirror of the selection restriction configured by Z ordering"); + _zOrderSelectionThresholdAction.setToolTip("Points below this Z-order dimension value are excluded from selection"); const auto updateActionsReadOnly = [this]() -> void { const auto isOutline = static_cast(_displayModeAction.getCurrentIndex()) == PointSelectionDisplayMode::Outline; @@ -66,6 +72,9 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) return; auto& scatterplotWidget = scatterplotPlugin->getScatterplotWidget(); + auto& zOrderingAction = dynamic_cast(parent())->getZOrderingAction(); + auto& thresholdEnabled = zOrderingAction.getSelectionThresholdEnabledAction(); + auto& threshold = zOrderingAction.getSelectionThresholdAction(); getPixelSelectionAction().initialize(&scatterplotWidget, &scatterplotWidget.getPixelSelectionTool(), { PixelSelectionType::Rectangle, @@ -88,9 +97,48 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) _outlineHaloEnabledAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineHaloEnabled()); _outlineOverrideColorAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineOverrideColor()); + if (threshold.getMinimum() > _zOrderSelectionThresholdAction.getMaximum()) { + _zOrderSelectionThresholdAction.setMaximum(threshold.getMaximum()); + _zOrderSelectionThresholdAction.setMinimum(threshold.getMinimum()); + } + else { + _zOrderSelectionThresholdAction.setMinimum(threshold.getMinimum()); + _zOrderSelectionThresholdAction.setMaximum(threshold.getMaximum()); + } + _zOrderSelectionThresholdAction.setValue(threshold.getValue()); + _zOrderSelectionThresholdAction.setEnabled(threshold.isEnabled()); + _zOrderSelectionThresholdEnabledAction.setChecked(thresholdEnabled.isChecked()); + _zOrderSelectionThresholdEnabledAction.setEnabled(thresholdEnabled.isEnabled()); + + connect(&thresholdEnabled, &ToggleAction::toggled, this, [this](bool checked) { + _zOrderSelectionThresholdEnabledAction.setChecked(checked); + }); + connect(&_zOrderSelectionThresholdEnabledAction, &ToggleAction::toggled, this, [thresholdEnabledAction = &thresholdEnabled](bool checked) { + thresholdEnabledAction->setChecked(checked); + }); + connect(&thresholdEnabled, &QAction::enabledChanged, this, [this](bool enabled) { + _zOrderSelectionThresholdEnabledAction.setEnabled(enabled); + }); + + connect(&threshold, &DecimalAction::valueChanged, this, [this](float value) { + _zOrderSelectionThresholdAction.setValue(value); + }); + connect(&_zOrderSelectionThresholdAction, &DecimalAction::valueChanged, this, [thresholdAction = &threshold](float value) { + thresholdAction->setValue(value); + }); + connect(&threshold, &DecimalAction::minimumChanged, this, [this](float minimum) { + _zOrderSelectionThresholdAction.setMinimum(minimum); + }); + connect(&threshold, &DecimalAction::maximumChanged, this, [this](float maximum) { + _zOrderSelectionThresholdAction.setMaximum(maximum); + }); + connect(&threshold, &QAction::enabledChanged, this, [this](bool enabled) { + _zOrderSelectionThresholdAction.setEnabled(enabled); + }); + connect(&_pixelSelectionAction.getSelectAllAction(), &QAction::triggered, [this, scatterplotPlugin]() { if (scatterplotPlugin->getPositionDataset().isValid()) - scatterplotPlugin->getPositionDataset()->selectAll(); + scatterplotPlugin->selectAllEligiblePoints(); }); connect(&_pixelSelectionAction.getClearSelectionAction(), &QAction::triggered, this, [this, scatterplotPlugin]() { @@ -100,7 +148,7 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) connect(&_pixelSelectionAction.getInvertSelectionAction(), &QAction::triggered, this, [this, scatterplotPlugin]() { if (scatterplotPlugin->getPositionDataset().isValid()) - scatterplotPlugin->getPositionDataset()->selectInvert(); + scatterplotPlugin->invertEligiblePointSelection(); }); connect(&_outlineScaleAction, &DecimalAction::valueChanged, this, [this, scatterplotPlugin](float value) { @@ -153,6 +201,8 @@ void SelectionAction::connectToPublicAction(WidgetAction* publicAction, bool rec actions().connectPrivateActionToPublicAction(&_outlineOpacityAction, &publicSelectionAction->getOutlineOpacityAction(), recursive); actions().connectPrivateActionToPublicAction(&_outlineHaloEnabledAction, &publicSelectionAction->getOutlineHaloEnabledAction(), recursive); actions().connectPrivateActionToPublicAction(&_freezeSelectionAction, &publicSelectionAction->getFreezeSelectionAction(), recursive); + actions().connectPrivateActionToPublicAction(&_zOrderSelectionThresholdEnabledAction, &publicSelectionAction->getZOrderSelectionThresholdEnabledAction(), recursive); + actions().connectPrivateActionToPublicAction(&_zOrderSelectionThresholdAction, &publicSelectionAction->getZOrderSelectionThresholdAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -171,6 +221,8 @@ void SelectionAction::disconnectFromPublicAction(bool recursive) actions().disconnectPrivateActionFromPublicAction(&_outlineOpacityAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_outlineHaloEnabledAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_freezeSelectionAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_zOrderSelectionThresholdEnabledAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_zOrderSelectionThresholdAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); @@ -204,4 +256,4 @@ QVariantMap SelectionAction::toVariantMap() const _freezeSelectionAction.insertIntoVariantMap(variantMap); return variantMap; -} \ No newline at end of file +} diff --git a/src/SelectionAction.h b/src/SelectionAction.h index d564c5e..db89fdc 100644 --- a/src/SelectionAction.h +++ b/src/SelectionAction.h @@ -65,6 +65,8 @@ class SelectionAction : public GroupAction DecimalAction& getOutlineOpacityAction() { return _outlineOpacityAction; } ToggleAction& getOutlineHaloEnabledAction() { return _outlineHaloEnabledAction; } ToggleAction& getFreezeSelectionAction() { return _freezeSelectionAction; } + ToggleAction& getZOrderSelectionThresholdEnabledAction() { return _zOrderSelectionThresholdEnabledAction; } + DecimalAction& getZOrderSelectionThresholdAction() { return _zOrderSelectionThresholdAction; } private: PixelSelectionAction _pixelSelectionAction; /** Pixel selection action */ @@ -75,10 +77,12 @@ class SelectionAction : public GroupAction DecimalAction _outlineOpacityAction; /** Selection outline opacity action */ ToggleAction _outlineHaloEnabledAction; /** Selection outline halo enabled action */ ToggleAction _freezeSelectionAction; /** Freeze selection action */ + ToggleAction _zOrderSelectionThresholdEnabledAction; /** Mirror of the Z-order selection restriction */ + DecimalAction _zOrderSelectionThresholdAction; /** Mirror of the minimum selectable Z-order value */ friend class mv::AbstractActionsManager; }; Q_DECLARE_METATYPE(SelectionAction) -inline const auto selectionActionMetaTypeId = qRegisterMetaType("SelectionAction"); \ No newline at end of file +inline const auto selectionActionMetaTypeId = qRegisterMetaType("SelectionAction"); diff --git a/src/ZOrderingAction.cpp b/src/ZOrderingAction.cpp index ff28bfc..1acce95 100644 --- a/src/ZOrderingAction.cpp +++ b/src/ZOrderingAction.cpp @@ -5,10 +5,16 @@ #include +#include +#include +#include + ZOrderingAction::ZOrderingAction(QObject* parent, const QString& title) : VerticalGroupAction(parent, title), _modeAction(this, "Mode", { "Insertion order", "Dimension", "Randomized" }), - _dimensionPickerAction(this, "Dimension") + _dimensionPickerAction(this, "Dimension"), + _selectionThresholdEnabledAction(this, "Restrict selection by Z order", false), + _selectionThresholdAction(this, "Minimum selectable value", 0.0f, 1.0f, 0.0f, 3) { setIconByName("layer-group"); setLabelSizingType(LabelSizingType::Auto); @@ -16,10 +22,16 @@ ZOrderingAction::ZOrderingAction(QObject* parent, const QString& title) : addAction(&_modeAction, OptionAction::HorizontalButtons); addAction(&_dimensionPickerAction); + addAction(&_selectionThresholdEnabledAction); + addAction(&_selectionThresholdAction); _modeAction.setToolTip("Choose how overlapping points are ordered"); _dimensionPickerAction.setToolTip("Dimension whose numerical values determine point depth"); + _selectionThresholdEnabledAction.setToolTip("Prevent points below a minimum Z-order value from being selected in this scatterplot"); + _selectionThresholdAction.setToolTip("Points below this dimension value are excluded from selection"); _dimensionPickerAction.setEnabled(false); + _selectionThresholdEnabledAction.setEnabled(false); + _selectionThresholdAction.setEnabled(false); } void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) @@ -36,6 +48,7 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) if (!positionDataset.isValid()) { _dimensionPickerAction.setPointsDataset(Dataset()); + _zOrderScalars.clear(); updateScatterplotWidget(); return; } @@ -48,6 +61,7 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) _dimensionPickerAction.setPointsDataset(positionDataset); _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + updateZOrderScalars(true); updateScatterplotWidget(); }; @@ -56,12 +70,22 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) }); connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this]() { + updateZOrderScalars(true); updateScatterplotWidget(); }); + connect(&_selectionThresholdEnabledAction, &ToggleAction::toggled, this, [this]() { + updateSelectionExclusions(); + }); + + connect(&_selectionThresholdAction, &DecimalAction::valueChanged, this, [this]() { + updateSelectionExclusions(); + }); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, updateDataset); connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataDimensionsChanged, this, updateDataset); connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataChanged, this, [this]() { + updateZOrderScalars(false); updateScatterplotWidget(); }); @@ -96,9 +120,101 @@ void ZOrderingAction::updateScatterplotWidget() } if (mode == Mode::Dimension && hasDataset) - _scatterplotPlugin->setZOrderDimension(_dimensionPickerAction.getCurrentDimensionIndex()); - else if (!hasDataset) - _scatterplotPlugin->setZOrderDimension(-1); + updateZOrderScalars(false); + + updateSelectionExclusions(); +} + +void ZOrderingAction::updateZOrderScalars(bool resetThreshold) +{ + _zOrderScalars.clear(); + + if (_scatterplotPlugin == nullptr) + return; + + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + const auto dimensionIndex = static_cast(_dimensionPickerAction.getCurrentDimensionIndex()); + + if (positionDataset.isValid() && dimensionIndex >= 0 && dimensionIndex < static_cast(positionDataset->getNumDimensions())) + positionDataset->extractDataForDimension(_zOrderScalars, dimensionIndex); + + _scatterplotPlugin->getScatterplotWidget().setZOrderScalars(_zOrderScalars); + + auto minimum = std::numeric_limits::max(); + auto maximum = std::numeric_limits::lowest(); + + for (const auto value : _zOrderScalars) { + if (!std::isfinite(value)) + continue; + + minimum = std::min(minimum, value); + maximum = std::max(maximum, value); + } + + if (minimum > maximum) { + minimum = 0.0f; + maximum = 1.0f; + } + + const auto previousThreshold = _selectionThresholdAction.getValue(); + + // NumericalAction clamps each endpoint against the other, so change the + // endpoint that expands the range first. + if (minimum > _selectionThresholdAction.getMaximum()) { + _selectionThresholdAction.setMaximum(maximum); + _selectionThresholdAction.setMinimum(minimum); + } + else { + _selectionThresholdAction.setMinimum(minimum); + _selectionThresholdAction.setMaximum(maximum); + } + + _selectionThresholdAction.setValue(resetThreshold ? minimum : std::clamp(previousThreshold, minimum, maximum)); + updateSelectionExclusions(); +} + +void ZOrderingAction::updateSelectionExclusions() +{ + if (_scatterplotPlugin == nullptr) + return; + + const auto mode = static_cast(_modeAction.getCurrentIndex()); + const auto restrictionAvailable = mode == Mode::Dimension && + _scatterplotPlugin->getPositionDataset().isValid() && + !_zOrderScalars.empty(); + + _selectionThresholdEnabledAction.setEnabled(restrictionAvailable); + _selectionThresholdAction.setEnabled(restrictionAvailable && _selectionThresholdEnabledAction.isChecked()); + + auto& scatterplotWidget = _scatterplotPlugin->getScatterplotWidget(); + auto& selectAllAction = dynamic_cast(parent())->getSelectionAction().getPixelSelectionAction().getSelectAllAction(); + + const auto restrictionActive = restrictionAvailable && _selectionThresholdEnabledAction.isChecked(); + + if (!restrictionActive) { + selectAllAction.setText("Select all"); + selectAllAction.setToolTip("Select all points"); + scatterplotWidget.clearSelectionExcludedIndices(); + _scatterplotPlugin->refreshSelection(); + return; + } + + std::vector excludedIndices; + excludedIndices.reserve(_zOrderScalars.size()); + + const auto threshold = _selectionThresholdAction.getValue(); + + for (std::uint32_t index = 0; index < _zOrderScalars.size(); ++index) { + const auto value = _zOrderScalars[index]; + + if (!std::isfinite(value) || value < threshold) + excludedIndices.push_back(index); + } + + scatterplotWidget.setSelectionExcludedIndices(excludedIndices); + selectAllAction.setText("Select all selectable points"); + selectAllAction.setToolTip(QString("Select all points that meet the Z-order threshold (%1 excluded)").arg(excludedIndices.size())); + _scatterplotPlugin->refreshSelection(); } QMenu* ZOrderingAction::getContextMenu(QWidget* parent) @@ -107,6 +223,9 @@ QMenu* ZOrderingAction::getContextMenu(QWidget* parent) menu->addAction(&_modeAction); menu->addAction(&_dimensionPickerAction); + menu->addSeparator(); + menu->addAction(&_selectionThresholdEnabledAction); + menu->addAction(&_selectionThresholdAction); return menu; } @@ -123,6 +242,8 @@ void ZOrderingAction::connectToPublicAction(WidgetAction* publicAction, bool rec if (recursive) { actions().connectPrivateActionToPublicAction(&_modeAction, &publicZOrderingAction->getModeAction(), recursive); actions().connectPrivateActionToPublicAction(&_dimensionPickerAction, &publicZOrderingAction->getDimensionPickerAction(), recursive); + actions().connectPrivateActionToPublicAction(&_selectionThresholdEnabledAction, &publicZOrderingAction->getSelectionThresholdEnabledAction(), recursive); + actions().connectPrivateActionToPublicAction(&_selectionThresholdAction, &publicZOrderingAction->getSelectionThresholdAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -136,6 +257,8 @@ void ZOrderingAction::disconnectFromPublicAction(bool recursive) if (recursive) { actions().disconnectPrivateActionFromPublicAction(&_modeAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_selectionThresholdEnabledAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_selectionThresholdAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); @@ -147,6 +270,8 @@ void ZOrderingAction::fromVariantMap(const QVariantMap& variantMap) _modeAction.fromParentVariantMap(variantMap); _dimensionPickerAction.fromParentVariantMap(variantMap); + _selectionThresholdEnabledAction.fromParentVariantMap(variantMap, true); + _selectionThresholdAction.fromParentVariantMap(variantMap, true); updateScatterplotWidget(); } @@ -156,6 +281,8 @@ QVariantMap ZOrderingAction::toVariantMap() const _modeAction.insertIntoVariantMap(variantMap); _dimensionPickerAction.insertIntoVariantMap(variantMap); + _selectionThresholdEnabledAction.insertIntoVariantMap(variantMap); + _selectionThresholdAction.insertIntoVariantMap(variantMap); return variantMap; } diff --git a/src/ZOrderingAction.h b/src/ZOrderingAction.h index a890689..b0eff96 100644 --- a/src/ZOrderingAction.h +++ b/src/ZOrderingAction.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include @@ -24,6 +26,7 @@ class ZOrderingAction : public VerticalGroupAction void initialize(ScatterplotPlugin* scatterplotPlugin); void updateScatterplotWidget(); + void updateSelectionExclusions(); QMenu* getContextMenu(QWidget* parent = nullptr) override; @@ -38,11 +41,18 @@ class ZOrderingAction : public VerticalGroupAction public: // Action getters OptionAction& getModeAction() { return _modeAction; } DimensionPickerAction& getDimensionPickerAction() { return _dimensionPickerAction; } + ToggleAction& getSelectionThresholdEnabledAction() { return _selectionThresholdEnabledAction; } + DecimalAction& getSelectionThresholdAction() { return _selectionThresholdAction; } private: + void updateZOrderScalars(bool resetThreshold); + ScatterplotPlugin* _scatterplotPlugin = nullptr; OptionAction _modeAction; DimensionPickerAction _dimensionPickerAction; + ToggleAction _selectionThresholdEnabledAction; + DecimalAction _selectionThresholdAction; + std::vector _zOrderScalars; friend class mv::AbstractActionsManager; }; From 727db450890e4cf0dc35cb04acc30f4ccad2521b Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Tue, 18 Aug 2026 10:23:55 +0200 Subject: [PATCH 3/4] Extract selection restriction into shared action Introduces a new `SelectionRestrictionAction` to manage selection filtering by dimension and value range, and wires it into `SelectionAction` with full serialization/linking support. This removes the old Z-order-specific threshold controls from `ZOrderingAction`, keeps Z-order focused on depth ordering, and adds a trigger to copy the current Z-order dimension into the selection restriction when needed. `SettingsAction` initialization/member order was also adjusted so the shared selection restriction is available where it is referenced. --- CMakeLists.txt | 2 + src/SelectionAction.cpp | 58 +------ src/SelectionAction.h | 8 +- src/SelectionRestrictionAction.cpp | 247 +++++++++++++++++++++++++++++ src/SelectionRestrictionAction.h | 54 +++++++ src/SettingsAction.cpp | 4 +- src/SettingsAction.h | 2 +- src/ZOrderingAction.cpp | 143 ++++------------- src/ZOrderingAction.h | 12 +- 9 files changed, 352 insertions(+), 178 deletions(-) create mode 100644 src/SelectionRestrictionAction.cpp create mode 100644 src/SelectionRestrictionAction.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 28e0c25..638ece1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,8 @@ set(Actions src/ScalarSourceAction.cpp src/SelectionAction.h src/SelectionAction.cpp + src/SelectionRestrictionAction.h + src/SelectionRestrictionAction.cpp src/SettingsAction.h src/SettingsAction.cpp src/SubsetAction.h diff --git a/src/SelectionAction.cpp b/src/SelectionAction.cpp index 8af81be..72b6aa8 100644 --- a/src/SelectionAction.cpp +++ b/src/SelectionAction.cpp @@ -16,8 +16,7 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : _outlineOpacityAction(this, "Opacity", 0.0f, 100.0f, 100.0f, 1), _outlineHaloEnabledAction(this, "Halo"), _freezeSelectionAction(this, "Freeze selection"), - _zOrderSelectionThresholdEnabledAction(this, "Restrict selection by Z order", false), - _zOrderSelectionThresholdAction(this, "Minimum selectable value", 0.0f, 1.0f, 0.0f, 3) + _selectionRestrictionAction(this, "Selection restriction") { setIconByName("mouse-pointer"); @@ -38,8 +37,7 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : addAction(&getOutlineOpacityAction()); addAction(&getOutlineHaloEnabledAction()); addAction(&getFreezeSelectionAction()); - addAction(&_zOrderSelectionThresholdEnabledAction); - addAction(&_zOrderSelectionThresholdAction); + addAction(&_selectionRestrictionAction); _pixelSelectionAction.getOverlayColorAction().setText("Color"); @@ -47,8 +45,6 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : _outlineScaleAction.setSuffix("%"); _outlineOpacityAction.setSuffix("%"); - _zOrderSelectionThresholdEnabledAction.setToolTip("Mirror of the selection restriction configured by Z ordering"); - _zOrderSelectionThresholdAction.setToolTip("Points below this Z-order dimension value are excluded from selection"); const auto updateActionsReadOnly = [this]() -> void { const auto isOutline = static_cast(_displayModeAction.getCurrentIndex()) == PointSelectionDisplayMode::Outline; @@ -72,9 +68,6 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) return; auto& scatterplotWidget = scatterplotPlugin->getScatterplotWidget(); - auto& zOrderingAction = dynamic_cast(parent())->getZOrderingAction(); - auto& thresholdEnabled = zOrderingAction.getSelectionThresholdEnabledAction(); - auto& threshold = zOrderingAction.getSelectionThresholdAction(); getPixelSelectionAction().initialize(&scatterplotWidget, &scatterplotWidget.getPixelSelectionTool(), { PixelSelectionType::Rectangle, @@ -97,44 +90,7 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) _outlineHaloEnabledAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineHaloEnabled()); _outlineOverrideColorAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineOverrideColor()); - if (threshold.getMinimum() > _zOrderSelectionThresholdAction.getMaximum()) { - _zOrderSelectionThresholdAction.setMaximum(threshold.getMaximum()); - _zOrderSelectionThresholdAction.setMinimum(threshold.getMinimum()); - } - else { - _zOrderSelectionThresholdAction.setMinimum(threshold.getMinimum()); - _zOrderSelectionThresholdAction.setMaximum(threshold.getMaximum()); - } - _zOrderSelectionThresholdAction.setValue(threshold.getValue()); - _zOrderSelectionThresholdAction.setEnabled(threshold.isEnabled()); - _zOrderSelectionThresholdEnabledAction.setChecked(thresholdEnabled.isChecked()); - _zOrderSelectionThresholdEnabledAction.setEnabled(thresholdEnabled.isEnabled()); - - connect(&thresholdEnabled, &ToggleAction::toggled, this, [this](bool checked) { - _zOrderSelectionThresholdEnabledAction.setChecked(checked); - }); - connect(&_zOrderSelectionThresholdEnabledAction, &ToggleAction::toggled, this, [thresholdEnabledAction = &thresholdEnabled](bool checked) { - thresholdEnabledAction->setChecked(checked); - }); - connect(&thresholdEnabled, &QAction::enabledChanged, this, [this](bool enabled) { - _zOrderSelectionThresholdEnabledAction.setEnabled(enabled); - }); - - connect(&threshold, &DecimalAction::valueChanged, this, [this](float value) { - _zOrderSelectionThresholdAction.setValue(value); - }); - connect(&_zOrderSelectionThresholdAction, &DecimalAction::valueChanged, this, [thresholdAction = &threshold](float value) { - thresholdAction->setValue(value); - }); - connect(&threshold, &DecimalAction::minimumChanged, this, [this](float minimum) { - _zOrderSelectionThresholdAction.setMinimum(minimum); - }); - connect(&threshold, &DecimalAction::maximumChanged, this, [this](float maximum) { - _zOrderSelectionThresholdAction.setMaximum(maximum); - }); - connect(&threshold, &QAction::enabledChanged, this, [this](bool enabled) { - _zOrderSelectionThresholdAction.setEnabled(enabled); - }); + _selectionRestrictionAction.initialize(scatterplotPlugin); connect(&_pixelSelectionAction.getSelectAllAction(), &QAction::triggered, [this, scatterplotPlugin]() { if (scatterplotPlugin->getPositionDataset().isValid()) @@ -201,8 +157,7 @@ void SelectionAction::connectToPublicAction(WidgetAction* publicAction, bool rec actions().connectPrivateActionToPublicAction(&_outlineOpacityAction, &publicSelectionAction->getOutlineOpacityAction(), recursive); actions().connectPrivateActionToPublicAction(&_outlineHaloEnabledAction, &publicSelectionAction->getOutlineHaloEnabledAction(), recursive); actions().connectPrivateActionToPublicAction(&_freezeSelectionAction, &publicSelectionAction->getFreezeSelectionAction(), recursive); - actions().connectPrivateActionToPublicAction(&_zOrderSelectionThresholdEnabledAction, &publicSelectionAction->getZOrderSelectionThresholdEnabledAction(), recursive); - actions().connectPrivateActionToPublicAction(&_zOrderSelectionThresholdAction, &publicSelectionAction->getZOrderSelectionThresholdAction(), recursive); + actions().connectPrivateActionToPublicAction(&_selectionRestrictionAction, &publicSelectionAction->getSelectionRestrictionAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -221,8 +176,7 @@ void SelectionAction::disconnectFromPublicAction(bool recursive) actions().disconnectPrivateActionFromPublicAction(&_outlineOpacityAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_outlineHaloEnabledAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_freezeSelectionAction, recursive); - actions().disconnectPrivateActionFromPublicAction(&_zOrderSelectionThresholdEnabledAction, recursive); - actions().disconnectPrivateActionFromPublicAction(&_zOrderSelectionThresholdAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_selectionRestrictionAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); @@ -240,6 +194,7 @@ void SelectionAction::fromVariantMap(const QVariantMap& variantMap) _outlineOpacityAction.fromParentVariantMap(variantMap); _outlineHaloEnabledAction.fromParentVariantMap(variantMap); _freezeSelectionAction.fromParentVariantMap(variantMap); + _selectionRestrictionAction.fromParentVariantMap(variantMap, true); } QVariantMap SelectionAction::toVariantMap() const @@ -254,6 +209,7 @@ QVariantMap SelectionAction::toVariantMap() const _outlineOpacityAction.insertIntoVariantMap(variantMap); _outlineHaloEnabledAction.insertIntoVariantMap(variantMap); _freezeSelectionAction.insertIntoVariantMap(variantMap); + _selectionRestrictionAction.insertIntoVariantMap(variantMap); return variantMap; } diff --git a/src/SelectionAction.h b/src/SelectionAction.h index db89fdc..0f3f574 100644 --- a/src/SelectionAction.h +++ b/src/SelectionAction.h @@ -3,6 +3,8 @@ #include #include +#include "SelectionRestrictionAction.h" + class ScatterplotPlugin; using namespace mv::gui; @@ -65,8 +67,7 @@ class SelectionAction : public GroupAction DecimalAction& getOutlineOpacityAction() { return _outlineOpacityAction; } ToggleAction& getOutlineHaloEnabledAction() { return _outlineHaloEnabledAction; } ToggleAction& getFreezeSelectionAction() { return _freezeSelectionAction; } - ToggleAction& getZOrderSelectionThresholdEnabledAction() { return _zOrderSelectionThresholdEnabledAction; } - DecimalAction& getZOrderSelectionThresholdAction() { return _zOrderSelectionThresholdAction; } + SelectionRestrictionAction& getSelectionRestrictionAction() { return _selectionRestrictionAction; } private: PixelSelectionAction _pixelSelectionAction; /** Pixel selection action */ @@ -77,8 +78,7 @@ class SelectionAction : public GroupAction DecimalAction _outlineOpacityAction; /** Selection outline opacity action */ ToggleAction _outlineHaloEnabledAction; /** Selection outline halo enabled action */ ToggleAction _freezeSelectionAction; /** Freeze selection action */ - ToggleAction _zOrderSelectionThresholdEnabledAction; /** Mirror of the Z-order selection restriction */ - DecimalAction _zOrderSelectionThresholdAction; /** Mirror of the minimum selectable Z-order value */ + SelectionRestrictionAction _selectionRestrictionAction; /** Dimension-based selection restriction */ friend class mv::AbstractActionsManager; }; diff --git a/src/SelectionRestrictionAction.cpp b/src/SelectionRestrictionAction.cpp new file mode 100644 index 0000000..b62ddad --- /dev/null +++ b/src/SelectionRestrictionAction.cpp @@ -0,0 +1,247 @@ +#include "SelectionRestrictionAction.h" + +#include "ScatterplotPlugin.h" +#include "ScatterplotWidget.h" +#include "SelectionAction.h" +#include "SettingsAction.h" + +#include +#include +#include + +SelectionRestrictionAction::SelectionRestrictionAction(QObject* parent, const QString& title) : + VerticalGroupAction(parent, title), + _enabledAction(this, "Restrict selection by dimension", false), + _dimensionPickerAction(this, "Dimension"), + _rangeAction(this, "Selectable range", util::NumericalRange(0.0f, 1.0f), util::NumericalRange(0.0f, 1.0f), 3) +{ + setIconByName("filter"); + setLabelSizingType(LabelSizingType::Auto); + setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + + addAction(&_enabledAction); + addAction(&_dimensionPickerAction); + addAction(&_rangeAction); + + _enabledAction.setToolTip("Only allow points within a dimension value range to be selected"); + _dimensionPickerAction.setToolTip("Dimension whose values determine whether points are selectable"); + _rangeAction.setToolTip("Inclusive range of dimension values for selectable points"); +} + +void SelectionRestrictionAction::initialize(ScatterplotPlugin* scatterplotPlugin) +{ + Q_ASSERT(scatterplotPlugin != nullptr); + + if (scatterplotPlugin == nullptr) + return; + + _scatterplotPlugin = scatterplotPlugin; + + connect(&_enabledAction, &ToggleAction::toggled, this, [this]() { + updateActionsReadOnly(); + updateSelectionExclusions(); + }); + + connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this]() { + updateDimensionValues(true); + }); + + connect(&_rangeAction, &DecimalRangeAction::rangeChanged, this, [this]() { + if (!_updatingRange) + updateSelectionExclusions(); + }); + + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, &SelectionRestrictionAction::updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataDimensionsChanged, this, &SelectionRestrictionAction::updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataChanged, this, [this]() { + updateDimensionValues(false); + }); + + updateDataset(); +} + +void SelectionRestrictionAction::updateDataset() +{ + if (_scatterplotPlugin == nullptr) + return; + + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + + if (!positionDataset.isValid()) { + _dimensionPickerAction.setPointsDataset(Dataset()); + _dimensionValues.clear(); + updateActionsReadOnly(); + updateSelectionExclusions(); + return; + } + + auto dimensionIndex = _dimensionPickerAction.getCurrentDimensionIndex(); + const auto numberOfDimensions = static_cast(positionDataset->getNumDimensions()); + + if (dimensionIndex < 0 || dimensionIndex >= numberOfDimensions) + dimensionIndex = 0; + + _dimensionPickerAction.setPointsDataset(positionDataset); + _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + updateDimensionValues(true); +} + +void SelectionRestrictionAction::updateDimensionValues(bool resetRange) +{ + _dimensionValues.clear(); + + if (_scatterplotPlugin == nullptr) + return; + + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + const auto dimensionIndex = _dimensionPickerAction.getCurrentDimensionIndex(); + + if (positionDataset.isValid() && dimensionIndex >= 0 && dimensionIndex < static_cast(positionDataset->getNumDimensions())) + positionDataset->extractDataForDimension(_dimensionValues, dimensionIndex); + + auto minimum = std::numeric_limits::max(); + auto maximum = std::numeric_limits::lowest(); + + for (const auto value : _dimensionValues) { + if (!std::isfinite(value)) + continue; + + minimum = std::min(minimum, value); + maximum = std::max(maximum, value); + } + + if (minimum > maximum) { + minimum = 0.0f; + maximum = 1.0f; + } + + const auto previousMinimum = _rangeAction.getMinimum(); + const auto previousMaximum = _rangeAction.getMaximum(); + + _updatingRange = true; + + if (minimum > _rangeAction.getLimitsMaximum()) { + _rangeAction.setLimitsMaximum(maximum); + _rangeAction.setLimitsMinimum(minimum); + } + else { + _rangeAction.setLimitsMinimum(minimum); + _rangeAction.setLimitsMaximum(maximum); + } + + if (resetRange) { + _rangeAction.setRange(util::NumericalRange(minimum, maximum)); + } + else { + const auto rangeMinimum = std::clamp(previousMinimum, minimum, maximum); + const auto rangeMaximum = std::clamp(previousMaximum, rangeMinimum, maximum); + + _rangeAction.setRange(util::NumericalRange(rangeMinimum, rangeMaximum)); + } + + _updatingRange = false; + + updateActionsReadOnly(); + updateSelectionExclusions(); +} + +void SelectionRestrictionAction::updateActionsReadOnly() +{ + const auto restrictionAvailable = _scatterplotPlugin != nullptr && + _scatterplotPlugin->getPositionDataset().isValid() && + !_dimensionValues.empty(); + + setEnabled(_scatterplotPlugin != nullptr && _scatterplotPlugin->getPositionDataset().isValid()); + _enabledAction.setEnabled(restrictionAvailable); + _dimensionPickerAction.setEnabled(restrictionAvailable); + _rangeAction.setEnabled(restrictionAvailable && _enabledAction.isChecked()); +} + +void SelectionRestrictionAction::updateSelectionExclusions() +{ + if (_scatterplotPlugin == nullptr) + return; + + auto& scatterplotWidget = _scatterplotPlugin->getScatterplotWidget(); + auto& selectAllAction = dynamic_cast(parent())->getPixelSelectionAction().getSelectAllAction(); + const auto restrictionActive = _enabledAction.isEnabled() && _enabledAction.isChecked(); + + if (!restrictionActive) { + selectAllAction.setText("Select all"); + selectAllAction.setToolTip("Select all points"); + scatterplotWidget.clearSelectionExcludedIndices(); + _scatterplotPlugin->refreshSelection(); + return; + } + + std::vector excludedIndices; + excludedIndices.reserve(_dimensionValues.size()); + + const auto minimum = _rangeAction.getMinimum(); + const auto maximum = _rangeAction.getMaximum(); + + for (std::uint32_t index = 0; index < _dimensionValues.size(); ++index) { + const auto value = _dimensionValues[index]; + + if (!std::isfinite(value) || value < minimum || value > maximum) + excludedIndices.push_back(index); + } + + scatterplotWidget.setSelectionExcludedIndices(excludedIndices); + selectAllAction.setText("Select all selectable points"); + selectAllAction.setToolTip(QString("Select all points within the dimension range (%1 excluded)").arg(excludedIndices.size())); + _scatterplotPlugin->refreshSelection(); +} + +void SelectionRestrictionAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) +{ + auto publicRestrictionAction = dynamic_cast(publicAction); + + Q_ASSERT(publicRestrictionAction != nullptr); + + if (publicRestrictionAction == nullptr) + return; + + if (recursive) { + actions().connectPrivateActionToPublicAction(&_enabledAction, &publicRestrictionAction->getEnabledAction(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionPickerAction, &publicRestrictionAction->getDimensionPickerAction(), recursive); + actions().connectPrivateActionToPublicAction(&_rangeAction, &publicRestrictionAction->getRangeAction(), recursive); + } + + GroupAction::connectToPublicAction(publicAction, recursive); +} + +void SelectionRestrictionAction::disconnectFromPublicAction(bool recursive) +{ + if (!isConnected()) + return; + + if (recursive) { + actions().disconnectPrivateActionFromPublicAction(&_enabledAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_rangeAction, recursive); + } + + GroupAction::disconnectFromPublicAction(recursive); +} + +void SelectionRestrictionAction::fromVariantMap(const QVariantMap& variantMap) +{ + GroupAction::fromVariantMap(variantMap); + + _dimensionPickerAction.fromParentVariantMap(variantMap); + _rangeAction.fromParentVariantMap(variantMap); + _enabledAction.fromParentVariantMap(variantMap); + updateSelectionExclusions(); +} + +QVariantMap SelectionRestrictionAction::toVariantMap() const +{ + auto variantMap = GroupAction::toVariantMap(); + + _enabledAction.insertIntoVariantMap(variantMap); + _dimensionPickerAction.insertIntoVariantMap(variantMap); + _rangeAction.insertIntoVariantMap(variantMap); + + return variantMap; +} diff --git a/src/SelectionRestrictionAction.h b/src/SelectionRestrictionAction.h new file mode 100644 index 0000000..75631a7 --- /dev/null +++ b/src/SelectionRestrictionAction.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#include + +class ScatterplotPlugin; + +using namespace mv::gui; + +/** Action for restricting selection eligibility to a dimension value range. */ +class SelectionRestrictionAction : public VerticalGroupAction +{ + Q_OBJECT + +public: + Q_INVOKABLE SelectionRestrictionAction(QObject* parent, const QString& title); + + void initialize(ScatterplotPlugin* scatterplotPlugin); + void updateSelectionExclusions(); + +protected: // Linking + void connectToPublicAction(WidgetAction* publicAction, bool recursive) override; + void disconnectFromPublicAction(bool recursive) override; + +public: // Serialization + void fromVariantMap(const QVariantMap& variantMap) override; + QVariantMap toVariantMap() const override; + +public: // Action getters + ToggleAction& getEnabledAction() { return _enabledAction; } + DimensionPickerAction& getDimensionPickerAction() { return _dimensionPickerAction; } + DecimalRangeAction& getRangeAction() { return _rangeAction; } + +private: + void updateDataset(); + void updateDimensionValues(bool resetRange); + void updateActionsReadOnly(); + + ScatterplotPlugin* _scatterplotPlugin = nullptr; + ToggleAction _enabledAction; + DimensionPickerAction _dimensionPickerAction; + DecimalRangeAction _rangeAction; + std::vector _dimensionValues; + bool _updatingRange = false; + + friend class mv::AbstractActionsManager; +}; + +Q_DECLARE_METATYPE(SelectionRestrictionAction) + +inline const auto selectionRestrictionActionMetaTypeId = qRegisterMetaType("SelectionRestrictionAction"); diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index d8aed0c..009745a 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -15,12 +15,12 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : _scatterplotPlugin(dynamic_cast(parent)), _renderModeAction(this, "Render Mode"), _positionAction(this, "Position"), + _selectionAction(this, "Selection"), _zOrderingAction(this, "Z ordering"), _plotAction(this, "Plot"), _coloringAction(this, "Coloring"), _subsetAction(this, "Subset"), _clusteringAction(this, "Clustering"), - _selectionAction(this, "Selection"), _exportAction(this, "Export"), _miscellaneousAction(this, "Miscellaneous"), _datasetsAction(this, "Datasets") @@ -28,10 +28,10 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : setConnectionPermissionsToForceNone(); _renderModeAction.initialize(_scatterplotPlugin); + _selectionAction.initialize(_scatterplotPlugin); _zOrderingAction.initialize(_scatterplotPlugin); _plotAction.initialize(_scatterplotPlugin); _subsetAction.initialize(_scatterplotPlugin); - _selectionAction.initialize(_scatterplotPlugin); _exportAction.initialize(_scatterplotPlugin); const auto updateEnabled = [this]() { diff --git a/src/SettingsAction.h b/src/SettingsAction.h index 362244c..f066cd7 100644 --- a/src/SettingsAction.h +++ b/src/SettingsAction.h @@ -74,12 +74,12 @@ class SettingsAction : public GroupAction ScatterplotPlugin* _scatterplotPlugin; /** Pointer to scatter plot plugin */ RenderModeAction _renderModeAction; /** Action for configuring render mode */ PositionAction _positionAction; /** Action for configuring point positions */ + SelectionAction _selectionAction; /** Action for selecting points */ ZOrderingAction _zOrderingAction; /** Action for configuring point z ordering */ PlotAction _plotAction; /** Action for configuring plot settings */ ColoringAction _coloringAction; /** Action for configuring point coloring */ SubsetAction _subsetAction; /** Action for creating subset(s) */ ClusteringAction _clusteringAction; /** Action for creating clusters */ - SelectionAction _selectionAction; /** Action for selecting points */ ExportAction _exportAction; /** Action for exporting */ MiscellaneousAction _miscellaneousAction; /** Action for miscellaneous settings */ DatasetsAction _datasetsAction; /** Action for picking dataset(s) */ diff --git a/src/ZOrderingAction.cpp b/src/ZOrderingAction.cpp index 1acce95..0f762da 100644 --- a/src/ZOrderingAction.cpp +++ b/src/ZOrderingAction.cpp @@ -2,19 +2,17 @@ #include "ScatterplotPlugin.h" #include "ScatterplotWidget.h" +#include "SelectionAction.h" +#include "SelectionRestrictionAction.h" +#include "SettingsAction.h" #include -#include -#include -#include - ZOrderingAction::ZOrderingAction(QObject* parent, const QString& title) : VerticalGroupAction(parent, title), _modeAction(this, "Mode", { "Insertion order", "Dimension", "Randomized" }), _dimensionPickerAction(this, "Dimension"), - _selectionThresholdEnabledAction(this, "Restrict selection by Z order", false), - _selectionThresholdAction(this, "Minimum selectable value", 0.0f, 1.0f, 0.0f, 3) + _useZOrderDimensionForSelectionAction(this, "Use Z-order dimension for selection") { setIconByName("layer-group"); setLabelSizingType(LabelSizingType::Auto); @@ -22,16 +20,14 @@ ZOrderingAction::ZOrderingAction(QObject* parent, const QString& title) : addAction(&_modeAction, OptionAction::HorizontalButtons); addAction(&_dimensionPickerAction); - addAction(&_selectionThresholdEnabledAction); - addAction(&_selectionThresholdAction); + addAction(&dynamic_cast(parent)->getSelectionAction().getSelectionRestrictionAction()); + addAction(&_useZOrderDimensionForSelectionAction); _modeAction.setToolTip("Choose how overlapping points are ordered"); _dimensionPickerAction.setToolTip("Dimension whose numerical values determine point depth"); - _selectionThresholdEnabledAction.setToolTip("Prevent points below a minimum Z-order value from being selected in this scatterplot"); - _selectionThresholdAction.setToolTip("Points below this dimension value are excluded from selection"); + _useZOrderDimensionForSelectionAction.setToolTip("Use the current Z-order dimension as the selection restriction dimension"); _dimensionPickerAction.setEnabled(false); - _selectionThresholdEnabledAction.setEnabled(false); - _selectionThresholdAction.setEnabled(false); + _useZOrderDimensionForSelectionAction.setEnabled(false); } void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) @@ -43,6 +39,8 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) _scatterplotPlugin = scatterplotPlugin; + auto& selectionRestrictionAction = dynamic_cast(parent())->getSelectionAction().getSelectionRestrictionAction(); + const auto updateDataset = [this]() { auto& positionDataset = _scatterplotPlugin->getPositionDataset(); @@ -61,7 +59,7 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) _dimensionPickerAction.setPointsDataset(positionDataset); _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); - updateZOrderScalars(true); + updateZOrderScalars(); updateScatterplotWidget(); }; @@ -70,22 +68,19 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) }); connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this]() { - updateZOrderScalars(true); + updateZOrderScalars(); updateScatterplotWidget(); }); - connect(&_selectionThresholdEnabledAction, &ToggleAction::toggled, this, [this]() { - updateSelectionExclusions(); - }); - - connect(&_selectionThresholdAction, &DecimalAction::valueChanged, this, [this]() { - updateSelectionExclusions(); + connect(&_useZOrderDimensionForSelectionAction, &TriggerAction::triggered, this, [this, selectionRestriction = &selectionRestrictionAction]() { + selectionRestriction->getDimensionPickerAction().setCurrentDimensionIndex(_dimensionPickerAction.getCurrentDimensionIndex()); + selectionRestriction->getEnabledAction().setChecked(true); }); connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, updateDataset); connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataDimensionsChanged, this, updateDataset); connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataChanged, this, [this]() { - updateZOrderScalars(false); + updateZOrderScalars(); updateScatterplotWidget(); }); @@ -104,6 +99,7 @@ void ZOrderingAction::updateScatterplotWidget() setEnabled(hasDataset); _dimensionPickerAction.setEnabled(hasDataset && mode == Mode::Dimension); + _useZOrderDimensionForSelectionAction.setEnabled(hasDataset && mode == Mode::Dimension); switch (mode) { case Mode::InsertionOrder: @@ -120,12 +116,10 @@ void ZOrderingAction::updateScatterplotWidget() } if (mode == Mode::Dimension && hasDataset) - updateZOrderScalars(false); - - updateSelectionExclusions(); + updateZOrderScalars(); } -void ZOrderingAction::updateZOrderScalars(bool resetThreshold) +void ZOrderingAction::updateZOrderScalars() { _zOrderScalars.clear(); @@ -139,82 +133,6 @@ void ZOrderingAction::updateZOrderScalars(bool resetThreshold) positionDataset->extractDataForDimension(_zOrderScalars, dimensionIndex); _scatterplotPlugin->getScatterplotWidget().setZOrderScalars(_zOrderScalars); - - auto minimum = std::numeric_limits::max(); - auto maximum = std::numeric_limits::lowest(); - - for (const auto value : _zOrderScalars) { - if (!std::isfinite(value)) - continue; - - minimum = std::min(minimum, value); - maximum = std::max(maximum, value); - } - - if (minimum > maximum) { - minimum = 0.0f; - maximum = 1.0f; - } - - const auto previousThreshold = _selectionThresholdAction.getValue(); - - // NumericalAction clamps each endpoint against the other, so change the - // endpoint that expands the range first. - if (minimum > _selectionThresholdAction.getMaximum()) { - _selectionThresholdAction.setMaximum(maximum); - _selectionThresholdAction.setMinimum(minimum); - } - else { - _selectionThresholdAction.setMinimum(minimum); - _selectionThresholdAction.setMaximum(maximum); - } - - _selectionThresholdAction.setValue(resetThreshold ? minimum : std::clamp(previousThreshold, minimum, maximum)); - updateSelectionExclusions(); -} - -void ZOrderingAction::updateSelectionExclusions() -{ - if (_scatterplotPlugin == nullptr) - return; - - const auto mode = static_cast(_modeAction.getCurrentIndex()); - const auto restrictionAvailable = mode == Mode::Dimension && - _scatterplotPlugin->getPositionDataset().isValid() && - !_zOrderScalars.empty(); - - _selectionThresholdEnabledAction.setEnabled(restrictionAvailable); - _selectionThresholdAction.setEnabled(restrictionAvailable && _selectionThresholdEnabledAction.isChecked()); - - auto& scatterplotWidget = _scatterplotPlugin->getScatterplotWidget(); - auto& selectAllAction = dynamic_cast(parent())->getSelectionAction().getPixelSelectionAction().getSelectAllAction(); - - const auto restrictionActive = restrictionAvailable && _selectionThresholdEnabledAction.isChecked(); - - if (!restrictionActive) { - selectAllAction.setText("Select all"); - selectAllAction.setToolTip("Select all points"); - scatterplotWidget.clearSelectionExcludedIndices(); - _scatterplotPlugin->refreshSelection(); - return; - } - - std::vector excludedIndices; - excludedIndices.reserve(_zOrderScalars.size()); - - const auto threshold = _selectionThresholdAction.getValue(); - - for (std::uint32_t index = 0; index < _zOrderScalars.size(); ++index) { - const auto value = _zOrderScalars[index]; - - if (!std::isfinite(value) || value < threshold) - excludedIndices.push_back(index); - } - - scatterplotWidget.setSelectionExcludedIndices(excludedIndices); - selectAllAction.setText("Select all selectable points"); - selectAllAction.setToolTip(QString("Select all points that meet the Z-order threshold (%1 excluded)").arg(excludedIndices.size())); - _scatterplotPlugin->refreshSelection(); } QMenu* ZOrderingAction::getContextMenu(QWidget* parent) @@ -223,9 +141,16 @@ QMenu* ZOrderingAction::getContextMenu(QWidget* parent) menu->addAction(&_modeAction); menu->addAction(&_dimensionPickerAction); - menu->addSeparator(); - menu->addAction(&_selectionThresholdEnabledAction); - menu->addAction(&_selectionThresholdAction); + + if (_scatterplotPlugin != nullptr) { + auto& selectionRestriction = dynamic_cast(this->parent())->getSelectionAction().getSelectionRestrictionAction(); + + menu->addSeparator(); + menu->addAction(&selectionRestriction.getEnabledAction()); + menu->addAction(&selectionRestriction.getDimensionPickerAction()); + menu->addAction(&selectionRestriction.getRangeAction()); + menu->addAction(&_useZOrderDimensionForSelectionAction); + } return menu; } @@ -242,8 +167,7 @@ void ZOrderingAction::connectToPublicAction(WidgetAction* publicAction, bool rec if (recursive) { actions().connectPrivateActionToPublicAction(&_modeAction, &publicZOrderingAction->getModeAction(), recursive); actions().connectPrivateActionToPublicAction(&_dimensionPickerAction, &publicZOrderingAction->getDimensionPickerAction(), recursive); - actions().connectPrivateActionToPublicAction(&_selectionThresholdEnabledAction, &publicZOrderingAction->getSelectionThresholdEnabledAction(), recursive); - actions().connectPrivateActionToPublicAction(&_selectionThresholdAction, &publicZOrderingAction->getSelectionThresholdAction(), recursive); + actions().connectPrivateActionToPublicAction(&_useZOrderDimensionForSelectionAction, &publicZOrderingAction->getUseZOrderDimensionForSelectionAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -257,8 +181,7 @@ void ZOrderingAction::disconnectFromPublicAction(bool recursive) if (recursive) { actions().disconnectPrivateActionFromPublicAction(&_modeAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); - actions().disconnectPrivateActionFromPublicAction(&_selectionThresholdEnabledAction, recursive); - actions().disconnectPrivateActionFromPublicAction(&_selectionThresholdAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_useZOrderDimensionForSelectionAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); @@ -270,8 +193,6 @@ void ZOrderingAction::fromVariantMap(const QVariantMap& variantMap) _modeAction.fromParentVariantMap(variantMap); _dimensionPickerAction.fromParentVariantMap(variantMap); - _selectionThresholdEnabledAction.fromParentVariantMap(variantMap, true); - _selectionThresholdAction.fromParentVariantMap(variantMap, true); updateScatterplotWidget(); } @@ -281,8 +202,6 @@ QVariantMap ZOrderingAction::toVariantMap() const _modeAction.insertIntoVariantMap(variantMap); _dimensionPickerAction.insertIntoVariantMap(variantMap); - _selectionThresholdEnabledAction.insertIntoVariantMap(variantMap); - _selectionThresholdAction.insertIntoVariantMap(variantMap); return variantMap; } diff --git a/src/ZOrderingAction.h b/src/ZOrderingAction.h index b0eff96..0b90465 100644 --- a/src/ZOrderingAction.h +++ b/src/ZOrderingAction.h @@ -1,8 +1,7 @@ #pragma once #include -#include -#include +#include #include #include @@ -26,7 +25,6 @@ class ZOrderingAction : public VerticalGroupAction void initialize(ScatterplotPlugin* scatterplotPlugin); void updateScatterplotWidget(); - void updateSelectionExclusions(); QMenu* getContextMenu(QWidget* parent = nullptr) override; @@ -41,17 +39,15 @@ class ZOrderingAction : public VerticalGroupAction public: // Action getters OptionAction& getModeAction() { return _modeAction; } DimensionPickerAction& getDimensionPickerAction() { return _dimensionPickerAction; } - ToggleAction& getSelectionThresholdEnabledAction() { return _selectionThresholdEnabledAction; } - DecimalAction& getSelectionThresholdAction() { return _selectionThresholdAction; } + TriggerAction& getUseZOrderDimensionForSelectionAction() { return _useZOrderDimensionForSelectionAction; } private: - void updateZOrderScalars(bool resetThreshold); + void updateZOrderScalars(); ScatterplotPlugin* _scatterplotPlugin = nullptr; OptionAction _modeAction; DimensionPickerAction _dimensionPickerAction; - ToggleAction _selectionThresholdEnabledAction; - DecimalAction _selectionThresholdAction; + TriggerAction _useZOrderDimensionForSelectionAction; std::vector _zOrderScalars; friend class mv::AbstractActionsManager; From 27b4fec6f39f5362450cd69fa689c5b4fb80e257 Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Wed, 19 Aug 2026 09:21:57 +0200 Subject: [PATCH 4/4] Couple z-ordering with selection restriction Replace the one-shot "use Z-order dimension" action with a persistent toggle that keeps Z-ordering and selection restriction in sync, including shared dimension updates and range enablement. This also loads selection settings earlier so the coupled state restores correctly, and hides labels in the selection restriction group for a cleaner embedded UI. --- src/SelectionRestrictionAction.cpp | 1 + src/SettingsAction.cpp | 2 +- src/ZOrderingAction.cpp | 133 +++++++++++++++++++++++++---- src/ZOrderingAction.h | 7 +- 4 files changed, 123 insertions(+), 20 deletions(-) diff --git a/src/SelectionRestrictionAction.cpp b/src/SelectionRestrictionAction.cpp index b62ddad..0d78f00 100644 --- a/src/SelectionRestrictionAction.cpp +++ b/src/SelectionRestrictionAction.cpp @@ -18,6 +18,7 @@ SelectionRestrictionAction::SelectionRestrictionAction(QObject* parent, const QS setIconByName("filter"); setLabelSizingType(LabelSizingType::Auto); setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + setShowLabels(false); addAction(&_enabledAction); addAction(&_dimensionPickerAction); diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index 009745a..c446a1b 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -75,12 +75,12 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) _datasetsAction.fromParentVariantMap(variantMap); _plotAction.fromParentVariantMap(variantMap); _positionAction.fromParentVariantMap(variantMap); + _selectionAction.fromParentVariantMap(variantMap); _zOrderingAction.fromParentVariantMap(variantMap, true); _coloringAction.fromParentVariantMap(variantMap); _subsetAction.fromParentVariantMap(variantMap, true); _clusteringAction.fromParentVariantMap(variantMap, true); _renderModeAction.fromParentVariantMap(variantMap); - _selectionAction.fromParentVariantMap(variantMap); _miscellaneousAction.fromParentVariantMap(variantMap); // Migrate projects saved before z ordering became a dedicated action. diff --git a/src/ZOrderingAction.cpp b/src/ZOrderingAction.cpp index 0f762da..60acfd7 100644 --- a/src/ZOrderingAction.cpp +++ b/src/ZOrderingAction.cpp @@ -7,27 +7,44 @@ #include "SettingsAction.h" #include +#include ZOrderingAction::ZOrderingAction(QObject* parent, const QString& title) : VerticalGroupAction(parent, title), _modeAction(this, "Mode", { "Insertion order", "Dimension", "Randomized" }), _dimensionPickerAction(this, "Dimension"), - _useZOrderDimensionForSelectionAction(this, "Use Z-order dimension for selection") + _restrictSelectionByZOrderAction(this, "Restrict selection by Z-order dimension", false) { - setIconByName("layer-group"); + setIconByName("sort"); setLabelSizingType(LabelSizingType::Auto); setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); addAction(&_modeAction, OptionAction::HorizontalButtons); addAction(&_dimensionPickerAction); - addAction(&dynamic_cast(parent)->getSelectionAction().getSelectionRestrictionAction()); - addAction(&_useZOrderDimensionForSelectionAction); + addAction(&_restrictSelectionByZOrderAction); + + auto& selectionRangeAction = dynamic_cast(parent)->getSelectionAction().getSelectionRestrictionAction().getRangeAction(); + + addAction(&selectionRangeAction, -1, [this, &selectionRangeAction](WidgetAction*, QWidget* widget) { + const auto updateReadOnly = [this, &selectionRangeAction, widget]() { + widget->setEnabled( + selectionRangeAction.isEnabled() && + _restrictSelectionByZOrderAction.isEnabled() && + _restrictSelectionByZOrderAction.isChecked()); + }; + + connect(&_restrictSelectionByZOrderAction, &ToggleAction::toggled, widget, updateReadOnly); + connect(&_restrictSelectionByZOrderAction, &QAction::enabledChanged, widget, updateReadOnly); + connect(&selectionRangeAction, &QAction::enabledChanged, widget, updateReadOnly); + + updateReadOnly(); + }); _modeAction.setToolTip("Choose how overlapping points are ordered"); _dimensionPickerAction.setToolTip("Dimension whose numerical values determine point depth"); - _useZOrderDimensionForSelectionAction.setToolTip("Use the current Z-order dimension as the selection restriction dimension"); + _restrictSelectionByZOrderAction.setToolTip("Restrict selection using the current Z-order dimension and the selectable range below"); _dimensionPickerAction.setEnabled(false); - _useZOrderDimensionForSelectionAction.setEnabled(false); + _restrictSelectionByZOrderAction.setEnabled(false); } void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) @@ -59,6 +76,7 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) _dimensionPickerAction.setPointsDataset(positionDataset); _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + updateZOrderScalars(); updateScatterplotWidget(); }; @@ -67,14 +85,85 @@ void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) updateScatterplotWidget(); }); - connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this]() { + connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this, selectionRestriction = &selectionRestrictionAction]() { updateZOrderScalars(); + + if (_restrictSelectionByZOrderAction.isChecked() && !_updatingCoupledRestriction) { + _updatingCoupledRestriction = true; + { + selectionRestriction->getDimensionPickerAction().setCurrentDimensionIndex(_dimensionPickerAction.getCurrentDimensionIndex()); + } + _updatingCoupledRestriction = false; + } + updateScatterplotWidget(); }); - connect(&_useZOrderDimensionForSelectionAction, &TriggerAction::triggered, this, [this, selectionRestriction = &selectionRestrictionAction]() { - selectionRestriction->getDimensionPickerAction().setCurrentDimensionIndex(_dimensionPickerAction.getCurrentDimensionIndex()); - selectionRestriction->getEnabledAction().setChecked(true); + connect(&_restrictSelectionByZOrderAction, &ToggleAction::toggled, this, [this, selectionRestriction = &selectionRestrictionAction](bool checked) { + if (_updatingCoupledRestriction) + return; + + const auto restrictionAvailable = _scatterplotPlugin->getPositionDataset().isValid() && + static_cast(_modeAction.getCurrentIndex()) == Mode::Dimension; + + if (checked && !restrictionAvailable) { + _updatingCoupledRestriction = true; + { + _restrictSelectionByZOrderAction.setChecked(false); + } + _updatingCoupledRestriction = false; + + return; + } + + _updatingCoupledRestriction = true; + + if (checked) { + selectionRestriction->getDimensionPickerAction().setCurrentDimensionIndex(_dimensionPickerAction.getCurrentDimensionIndex()); + selectionRestriction->getEnabledAction().setChecked(true); + } else { + selectionRestriction->getEnabledAction().setChecked(false); + } + + _updatingCoupledRestriction = false; + }); + + connect(&selectionRestrictionAction.getEnabledAction(), &ToggleAction::toggled, this, [this, selectionRestriction = &selectionRestrictionAction](bool checked) { + if (_updatingCoupledRestriction) + return; + + const auto restrictionAvailable = _scatterplotPlugin->getPositionDataset().isValid() && + static_cast(_modeAction.getCurrentIndex()) == Mode::Dimension; + + _updatingCoupledRestriction = true; + + if (checked && restrictionAvailable) { + const QSignalBlocker dimensionBlocker(&_dimensionPickerAction); + _dimensionPickerAction.setCurrentDimensionIndex(selectionRestriction->getDimensionPickerAction().getCurrentDimensionIndex()); + updateZOrderScalars(); + } + + _restrictSelectionByZOrderAction.setChecked(checked && restrictionAvailable); + + _updatingCoupledRestriction = false; + }); + + connect(&selectionRestrictionAction.getDimensionPickerAction(), &DimensionPickerAction::currentDimensionIndexChanged, this, [this, selectionRestriction = &selectionRestrictionAction](std::int32_t dimensionIndex) { + if (_updatingCoupledRestriction || !selectionRestriction->getEnabledAction().isChecked() || + static_cast(_modeAction.getCurrentIndex()) != Mode::Dimension) + return; + + _updatingCoupledRestriction = true; + + { + const QSignalBlocker dimensionBlocker(&_dimensionPickerAction); + _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + } + + _restrictSelectionByZOrderAction.setChecked(true); + + updateZOrderScalars(); + _updatingCoupledRestriction = false; }); connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, updateDataset); @@ -99,7 +188,21 @@ void ZOrderingAction::updateScatterplotWidget() setEnabled(hasDataset); _dimensionPickerAction.setEnabled(hasDataset && mode == Mode::Dimension); - _useZOrderDimensionForSelectionAction.setEnabled(hasDataset && mode == Mode::Dimension); + _restrictSelectionByZOrderAction.setEnabled(hasDataset && mode == Mode::Dimension); + + auto& selectionRestriction = dynamic_cast(parent())->getSelectionAction().getSelectionRestrictionAction(); + const auto restrictionCoupled = hasDataset && mode == Mode::Dimension && selectionRestriction.getEnabledAction().isChecked(); + + _updatingCoupledRestriction = true; + + if (restrictionCoupled) { + const QSignalBlocker dimensionBlocker(&_dimensionPickerAction); + _dimensionPickerAction.setCurrentDimensionIndex(selectionRestriction.getDimensionPickerAction().getCurrentDimensionIndex()); + } + + _restrictSelectionByZOrderAction.setChecked(restrictionCoupled); + + _updatingCoupledRestriction = false; switch (mode) { case Mode::InsertionOrder: @@ -146,10 +249,8 @@ QMenu* ZOrderingAction::getContextMenu(QWidget* parent) auto& selectionRestriction = dynamic_cast(this->parent())->getSelectionAction().getSelectionRestrictionAction(); menu->addSeparator(); - menu->addAction(&selectionRestriction.getEnabledAction()); - menu->addAction(&selectionRestriction.getDimensionPickerAction()); + menu->addAction(&_restrictSelectionByZOrderAction); menu->addAction(&selectionRestriction.getRangeAction()); - menu->addAction(&_useZOrderDimensionForSelectionAction); } return menu; @@ -167,7 +268,7 @@ void ZOrderingAction::connectToPublicAction(WidgetAction* publicAction, bool rec if (recursive) { actions().connectPrivateActionToPublicAction(&_modeAction, &publicZOrderingAction->getModeAction(), recursive); actions().connectPrivateActionToPublicAction(&_dimensionPickerAction, &publicZOrderingAction->getDimensionPickerAction(), recursive); - actions().connectPrivateActionToPublicAction(&_useZOrderDimensionForSelectionAction, &publicZOrderingAction->getUseZOrderDimensionForSelectionAction(), recursive); + actions().connectPrivateActionToPublicAction(&_restrictSelectionByZOrderAction, &publicZOrderingAction->getRestrictSelectionByZOrderAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -181,7 +282,7 @@ void ZOrderingAction::disconnectFromPublicAction(bool recursive) if (recursive) { actions().disconnectPrivateActionFromPublicAction(&_modeAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); - actions().disconnectPrivateActionFromPublicAction(&_useZOrderDimensionForSelectionAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_restrictSelectionByZOrderAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); diff --git a/src/ZOrderingAction.h b/src/ZOrderingAction.h index 0b90465..2beadd2 100644 --- a/src/ZOrderingAction.h +++ b/src/ZOrderingAction.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include @@ -39,7 +39,7 @@ class ZOrderingAction : public VerticalGroupAction public: // Action getters OptionAction& getModeAction() { return _modeAction; } DimensionPickerAction& getDimensionPickerAction() { return _dimensionPickerAction; } - TriggerAction& getUseZOrderDimensionForSelectionAction() { return _useZOrderDimensionForSelectionAction; } + ToggleAction& getRestrictSelectionByZOrderAction() { return _restrictSelectionByZOrderAction; } private: void updateZOrderScalars(); @@ -47,8 +47,9 @@ class ZOrderingAction : public VerticalGroupAction ScatterplotPlugin* _scatterplotPlugin = nullptr; OptionAction _modeAction; DimensionPickerAction _dimensionPickerAction; - TriggerAction _useZOrderDimensionForSelectionAction; + ToggleAction _restrictSelectionByZOrderAction; std::vector _zOrderScalars; + bool _updatingCoupledRestriction = false; friend class mv::AbstractActionsManager; };