From 28e77069fbcf4e7acd1d9043d0230e180b7f0de6 Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Mon, 30 Mar 2026 17:39:40 +0200 Subject: [PATCH 01/17] Fix warnings and update points (#240) * Update number of points to uint64 * Use reference dataset * Add some const * Rename lambda capture variable to not shadow function paramters * More uint64 * Set MSVC warning level to W3 * We only want one dataset --- CMakeLists.txt | 2 +- src/MappingUtils.cpp | 18 +++++++++--------- src/MappingUtils.h | 8 ++++---- src/ScatterplotPlugin.cpp | 14 +++++++------- src/ScatterplotPlugin.h | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b9674d0..59655d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ set(CMAKE_AUTORCC ON) set(CMAKE_AUTOMOC ON) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DWIN32 /EHsc /MP /permissive- /Zc:__cplusplus") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DWIN32 /EHsc /W3 /MP /permissive- /Zc:__cplusplus") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MD") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD") diff --git a/src/MappingUtils.cpp b/src/MappingUtils.cpp index e238064..5483a24 100644 --- a/src/MappingUtils.cpp +++ b/src/MappingUtils.cpp @@ -14,7 +14,7 @@ #include #include -std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping) { +std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping) { const std::vector& linkedDatas = source->getLinkedData(); if (linkedDatas.empty()) @@ -34,18 +34,18 @@ std::pair getSelectionMapping(const mv::Dat return { nullptr, 0 }; } -std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions) { - auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset& positions) -> bool { +std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions) { + auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset& positions_) -> bool { const mv::Dataset mapTargetData = linkedData.getTargetDataset(); - return mapTargetData == positions || parentHasSameNumPoints(mapTargetData, positions); + return mapTargetData == positions_ || parentHasSameNumPoints(mapTargetData, positions_); }; return getSelectionMapping(colors, positions, testTargetAndParent); } -std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors) { - auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset& colors) -> bool { - return linkedData.getTargetDataset() == colors; +std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors) { + auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset& colors_) -> bool { + return linkedData.getTargetDataset() == colors_; }; auto [mapping, numTargetPoints] = getSelectionMapping(positions, colors, testTarget); @@ -58,7 +58,7 @@ std::pair getSelectionMappingPositionsToCol return { mapping, numTargetPoints }; } -std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors) { +std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors) { if (!positions->isDerivedData()) return { nullptr, 0 }; @@ -77,7 +77,7 @@ bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_ std::uint32_t count = 0; for (const auto& [key, vec] : linkedMap) { - for (std::uint32_t val : vec) { + for (const std::uint32_t val : vec) { if (val >= numPointsInTarget) continue; // Skip values that are too large if (!found[val]) { diff --git a/src/MappingUtils.h b/src/MappingUtils.h index 62a4cba..6229dcb 100644 --- a/src/MappingUtils.h +++ b/src/MappingUtils.h @@ -39,17 +39,17 @@ using LinkedDataCondition = std::function getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping); +std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping); // Returns a mapping (linked data) from colors whose target is positions or whose target's parent has the same number of points as positions -std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions); +std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions); // Returns a mapping (linked data) from positions whose target is colors or // a mapping from positions' parent whose target is colors if the number of data points match -std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors); +std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors); // Returns a mapping (linked data) from positions' source data whose target is colors -std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors); +std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors); // Check if the mapping is surjective, i.e. hits all elements in the target bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_t numPointsInTarget); diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index c071758..42d2939 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -138,10 +138,10 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : if (datasetsMimeData == nullptr) return dropRegions; - if (datasetsMimeData->getDatasets().count() > 1) + if (datasetsMimeData->getDatasetsCount() != 1) return dropRegions; - const auto dataset = datasetsMimeData->getDatasets().first(); + const auto& dataset = datasetsMimeData->getDatasetsRef().first(); const auto datasetGuiName = dataset->text(); const auto datasetId = dataset->getId(); const auto dataType = dataset->getDataType(); @@ -244,13 +244,13 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : { // Check to set whether the number of data points comprised throughout all clusters is the same number // as the number of data points in the dataset we are trying to color - int totalNumIndices = 0; + std::uint64_t totalNumIndices = 0; for (const Cluster& cluster : candidateDataset->getClusters()) { totalNumIndices += cluster.getIndices().size(); } - int totalNumPoints = 0; + std::uint64_t totalNumPoints = 0; if (_positionDataset->isDerivedData()) totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); else @@ -755,7 +755,7 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std const mv::SelectionMap::Map& mapColorsToPositions = selectionMapping->getMapping().getMap(); for (const auto& [fromColorID, vecOfPositionIDs] : mapColorsToPositions) { - for (std::uint32_t toPositionID : vecOfPositionIDs) { + for (const std::uint32_t toPositionID : vecOfPositionIDs) { mappedColorScalars[toPositionID] = colorScalars[fromColorID]; } } @@ -775,7 +775,7 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std for (const auto& [fromPositionID, vecOfColorIDs] : mapPositionsToColors) { if (mappedColorScalars[fromPositionID] != std::numeric_limits::lowest()) continue; - for (std::uint32_t toColorID : vecOfColorIDs) { + for (const std::uint32_t toColorID : vecOfColorIDs) { mappedColorScalars[fromPositionID] = colorScalars[toColorID]; } } @@ -846,7 +846,7 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) return; // Get global indices from the position dataset - int totalNumPoints = 0; + std::uint64_t totalNumPoints = 0; if (_positionDataset->isDerivedData()) totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); else diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index 8649f82..437876b 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -119,7 +119,7 @@ class ScatterplotPlugin : public ViewPlugin Dataset _positionDataset; /** Smart pointer to points dataset for point position */ Dataset _positionSourceDataset; /** Smart pointer to source of the points dataset for point position (if any) */ std::vector _positions; /** Point positions */ - unsigned int _numPoints; /** Number of point positions */ + std::uint64_t _numPoints; /** Number of point positions */ QPointer _settingsAction; /** Group action for all settings */ QPointer _primaryToolbarAction; /** Horizontal toolbar for primary content */ QRectF _selectionBoundaries; /** Boundaries of the selection */ From 109e3d170e1fc24459a2e56f992e631bd45d8ad6 Mon Sep 17 00:00:00 2001 From: Julian Thijssen Date: Fri, 17 Apr 2026 16:13:24 +0200 Subject: [PATCH 02/17] Update core requirement due to previous commit --- PluginInfo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginInfo.json b/PluginInfo.json index a09ee50..9dc9e34 100644 --- a/PluginInfo.json +++ b/PluginInfo.json @@ -2,7 +2,7 @@ "name" : "Scatterplot View", "version" : { "plugin" : "1.0.0", - "core" : ["1.3"] + "core" : ["1.5"] }, "type" : "View", "dependencies" : ["Points"] From a555b331a1d581b2c9d6cc3ed6ad8300435290f4 Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Tue, 30 Jun 2026 18:51:13 +0200 Subject: [PATCH 03/17] Adhere to new serialization API (#243) * Use new getter for clarity (avoid negation) (#242) * Use `mv_project_defaults()` for setting CMake defaults (#241) * Use mv project defaults * Simplify unity build setup * Prefer target based properties * Set cache variable instead of normal variable for CMake option * Adhere to revamped core --------- Co-authored-by: Alexander Vieth --- CMakeLists.txt | 18 +++++------------- conanfile.py | 2 +- src/ScalarAction.cpp | 4 ++-- src/ScatterplotPlugin.cpp | 3 ++- src/SettingsAction.cpp | 4 ++-- 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 59655d7..39453a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,17 +15,7 @@ PROJECT(${PROJECT} # ----------------------------------------------------------------------------- # CMake Options # ----------------------------------------------------------------------------- -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOMOC ON) - -if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DWIN32 /EHsc /W3 /MP /permissive- /Zc:__cplusplus") - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") - set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MD") - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD") -endif() # ----------------------------------------------------------------------------- # Dependencies @@ -33,6 +23,7 @@ endif() find_package(Qt6 COMPONENTS Widgets WebEngineWidgets OpenGL OpenGLWidgets REQUIRED) find_package(ManiVault COMPONENTS Core PointData ClusterData ColorData ImageData CONFIG QUIET) +mv_project_defaults() # ----------------------------------------------------------------------------- # Source files @@ -116,9 +107,10 @@ target_include_directories(${PROJECT} PRIVATE "${ManiVault_INCLUDE_DIR}") # ----------------------------------------------------------------------------- target_compile_features(${PROJECT} PRIVATE cxx_std_20) -if(MV_UNITY_BUILD) - set_target_properties(${PROJECT} PROPERTIES UNITY_BUILD ON) -endif() +set_target_properties(${PROJECT} PROPERTIES + AUTOMOC ON + UNITY_BUILD ${MV_UNITY_BUILD} +) # ----------------------------------------------------------------------------- # Target library linking diff --git a/conanfile.py b/conanfile.py index 3b84dde..73491bf 100644 --- a/conanfile.py +++ b/conanfile.py @@ -105,7 +105,7 @@ def generate(self): tc.variables["ManiVault_DIR"] = manivault_dir # Set some build options - tc.variables["MV_UNITY_BUILD"] = "ON" + tc.cache_variables["MV_UNITY_BUILD"] = True tc.generate() diff --git a/src/ScalarAction.cpp b/src/ScalarAction.cpp index f500bac..57653e4 100644 --- a/src/ScalarAction.cpp +++ b/src/ScalarAction.cpp @@ -184,7 +184,7 @@ void ScalarAction::fromVariantMap(const QVariantMap& variantMap) _magnitudeAction.fromParentVariantMap(variantMap); _sourceAction.fromParentVariantMap(variantMap); - _sourceDatasetPickerAction.fromParentVariantMap(variantMap); + //_sourceDatasetPickerAction.fromParentVariantMap(variantMap); } QVariantMap ScalarAction::toVariantMap() const @@ -193,7 +193,7 @@ QVariantMap ScalarAction::toVariantMap() const _magnitudeAction.insertIntoVariantMap(variantMap); _sourceAction.insertIntoVariantMap(variantMap); - _sourceDatasetPickerAction.insertIntoVariantMap(variantMap); + //_sourceDatasetPickerAction.insertIntoVariantMap(variantMap); return variantMap; } diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 42d2939..531eb45 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -569,7 +570,7 @@ void ScatterplotPlugin::selectPoints() auto& navigationAction = navigator.getNavigationAction(); - navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && !navigationAction.getFreezeNavigation().isChecked()); + navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && navigationAction.isNavigationActive()); _positionDataset->setSelectionIndices(targetSelectionIndices); diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index 03d0f82..f0739dc 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -70,8 +70,8 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) _plotAction.fromParentVariantMap(variantMap); _positionAction.fromParentVariantMap(variantMap); _coloringAction.fromParentVariantMap(variantMap); - _subsetAction.fromParentVariantMap(variantMap); - _clusteringAction.fromParentVariantMap(variantMap); + _subsetAction.fromParentVariantMap(variantMap, true); + _clusteringAction.fromParentVariantMap(variantMap, true); _renderModeAction.fromParentVariantMap(variantMap); _selectionAction.fromParentVariantMap(variantMap); _miscellaneousAction.fromParentVariantMap(variantMap); From 09914e23f036267228d62f415c5345a54fc1e23e Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Tue, 21 Jul 2026 12:26:17 +0200 Subject: [PATCH 04/17] Set current point dataset when opacity dataset changed --- src/PointPlotAction.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PointPlotAction.cpp b/src/PointPlotAction.cpp index e5c33ab..2425ab6 100644 --- a/src/PointPlotAction.cpp +++ b/src/PointPlotAction.cpp @@ -162,6 +162,10 @@ void PointPlotAction::initialize(ScatterplotPlugin* scatterplotPlugin) connect(&_opacityAction, &ScalarAction::magnitudeChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::offsetChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::sourceSelectionChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); + connect(&_opacityAction.getSourceDatasetPickerAction(), &DatasetPickerAction::datasetPicked, this, [this](Dataset<> picked) -> void { + setCurrentPointOpacityDataset(Dataset(picked)); + }); + connect(&_opacityAction, &ScalarAction::sourceDataChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::scalarRangeChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); } From 004ae93e1bdff658fad19f297859546b2704fda9 Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Tue, 21 Jul 2026 12:26:30 +0200 Subject: [PATCH 05/17] Add extra null guard --- src/ScalarAction.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ScalarAction.cpp b/src/ScalarAction.cpp index 57653e4..24f86b6 100644 --- a/src/ScalarAction.cpp +++ b/src/ScalarAction.cpp @@ -29,13 +29,16 @@ ScalarAction::ScalarAction(QObject* parent, const QString& title, const float& m if (auto scatterplotPlugin = dynamic_cast(findPluginAncestor())) { auto positionDataset = scatterplotPlugin->getPositionDataset(); auto scalarSourcePointsDataset = Dataset(getCurrentDataset()); - const auto numScalars = scalarSourcePointsDataset->getNumPoints(); - const auto numPositions = positionDataset->getNumPoints(); - if (numScalars != numPositions) { - emitSourceSelectionChanged = false; + if (scalarSourcePointsDataset.isValid() && positionDataset.isValid()) { + const auto numScalars = scalarSourcePointsDataset->getNumPoints(); + const auto numPositions = positionDataset->getNumPoints(); - scatterplotPlugin->addNotification(QString("The number of points in the scalar source dataset does not match the number of points in the position dataset. (numPositions=%1, numScalars:%2)").arg(QString::number(numPositions), QString::number(numScalars))); + if (numScalars != numPositions) { + emitSourceSelectionChanged = false; + + scatterplotPlugin->addNotification(QString("The number of points in the scalar source dataset does not match the number of points in the position dataset. (numPositions=%1, numScalars:%2)").arg(QString::number(numPositions), QString::number(numScalars))); + } } } } else { From 9810b4cbeb009a88be7a703fb4b3bb9ac0612c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20H=C3=B6llt?= Date: Thu, 23 Jul 2026 17:25:31 +0200 Subject: [PATCH 06/17] Extends coloring options for scatterplot (addressing issue #24) (#247) Adds 2D and 3D coloring options. 2D allows arbitrary 2 channels using the build in 2D colormaps 3D allows arbitrary 3 channels mapping directly to RGB (normalized in shader) Modes are automatically picked when datasets with exactly 2 or 3 channels are set as color or can be manually set using the extended color action Renames 2D colormaps according to their authors --- src/ColoringAction.cpp | 206 +++++++++++++++++++++++++++++++++++--- src/ColoringAction.h | 16 ++- src/ScatterplotPlugin.cpp | 71 +++++++++++-- src/ScatterplotPlugin.h | 28 ++++++ src/ScatterplotWidget.cpp | 16 ++- src/ScatterplotWidget.h | 6 ++ 6 files changed, 319 insertions(+), 24 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index c3fca1f..71db211 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -16,7 +16,10 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : _colorByModel(this), _colorByAction(this, "Color by"), _constantColorAction(this, "Constant color", DEFAULT_CONSTANT_COLOR), - _dimensionAction(this, "Dimension"), + _colorSpaceAction(this, "Color space", { "Scalar (1D)", "Duo (2D)", "RGB" }, "Scalar (1D)"), + _dimensionAction(this, "Dimension 1"), + _dimensionAction2(this, "Dimension 2"), + _dimensionAction3(this, "Dimension 3"), _colorMap1DAction(this, "1D Color map"), _colorMap2DAction(this, "2D Color map") { @@ -26,9 +29,14 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : addAction(&_colorByAction); addAction(&_constantColorAction); - addAction(&_colorMap2DAction); + addAction(&_colorSpaceAction); addAction(&_colorMap1DAction); + addAction(&_colorMap2DAction); addAction(&_dimensionAction); + addAction(&_dimensionAction2); + addAction(&_dimensionAction3); + + _colorSpaceAction.setToolTip("Color space for data-driven coloring"); _scatterplotPlugin->getWidget().addAction(&_colorByAction); _scatterplotPlugin->getWidget().addAction(&_dimensionAction); @@ -86,28 +94,51 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : connect(&_currentColorPointsDataset, &Dataset::dataDimensionsChanged, this, [this]() { if (_currentColorPointsDataset.isValid()) { _dimensionAction.setPointsDataset(_currentColorPointsDataset); + _dimensionAction2.setPointsDataset(_currentColorPointsDataset); + _dimensionAction3.setPointsDataset(_currentColorPointsDataset); updateScatterPlotWidgetColors(); } }); _dimensionAction.setPointsDataset(_currentColorPointsDataset); + _dimensionAction2.setPointsDataset(_currentColorPointsDataset); + _dimensionAction3.setPointsDataset(_currentColorPointsDataset); + + // Auto-select the color space for datasets with exactly two or three channels + if (!mv::projects().isOpeningProject()) { + const auto numDimensions = static_cast(_currentColorPointsDataset->getNumDimensions()); + + if (numDimensions == 2) { + _colorSpaceAction.setCurrentIndex(1); // Duo (2D) + applyDefaultChannels(); // also apply defaults when the index was already Duo + } + else if (numDimensions == 3) { + _colorSpaceAction.setCurrentIndex(2); // RGB + applyDefaultChannels(); // also apply defaults when the index was already RGB + } + } } else { _dimensionAction.setPointsDataset(Dataset()); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } } else { _dimensionAction.setPointsDataset(Dataset()); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } - //_dimensionAction.setVisible(currentColorDatasetTypeIsPointType); emit currentColorDatasetChanged(currentColorDataset); } else { _dimensionAction.setPointsDataset(Dataset()); - //_dimensionAction.setVisible(false); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } + updateChannelActionsReadOnly(); updateScatterPlotWidgetColors(); updateScatterplotWidgetColorMap(); updateColorMapActionScalarRange(); @@ -142,7 +173,25 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : updateColorMapActionsReadOnly(); updateColorMapActionScalarRange(); }); - + + connect(&_dimensionAction2, &DimensionPickerAction::currentDimensionIndexChanged, this, [this](const int32_t& currentDimensionIndex) { + updateScatterPlotWidgetColors(); + }); + + connect(&_dimensionAction3, &DimensionPickerAction::currentDimensionIndexChanged, this, [this](const int32_t& currentDimensionIndex) { + updateScatterPlotWidgetColors(); + }); + + connect(&_colorSpaceAction, &OptionAction::currentIndexChanged, this, [this](const std::int32_t& currentIndex) { + if (!mv::projects().isOpeningProject()) + applyDefaultChannels(); + + updateChannelActionsReadOnly(); + updateScatterPlotWidgetColors(); + updateScatterplotWidgetColorMap(); + updateColorMapActionsReadOnly(); + }); + connect(&_constantColorAction, &ColorAction::colorChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); connect(&_colorMap1DAction, &ColorMapAction::imageChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); connect(&_colorMap2DAction, &ColorMapAction::imageChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); @@ -160,6 +209,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : updateScatterplotWidgetColorMap(); updateColorMapActionScalarRange(); + updateChannelActionsReadOnly(); _scatterplotPlugin->getScatterplotWidget().setColoringMode(ScatterplotWidget::ColoringMode::Constant); } @@ -251,10 +301,38 @@ void ColoringAction::updateScatterPlotWidgetColors() if (currentColorDataset->getDataType() == ClusterType) _scatterplotPlugin->loadColors(currentColorDataset.get()); else { - const auto currentDimensionIndex = _dimensionAction.getCurrentDimensionIndex(); + const auto dimension1 = _dimensionAction.getCurrentDimensionIndex(); + + if (dimension1 < 0) + return; + + switch (_colorSpaceAction.getCurrentIndex()) + { + case 1: // Duo (2D) + { + const auto dimension2 = _dimensionAction2.getCurrentDimensionIndex(); + + if (dimension2 >= 0) + _scatterplotPlugin->loadColors2D(currentColorDataset.get(), dimension1, dimension2); + + break; + } + + case 2: // RGB + { + const auto dimension2 = _dimensionAction2.getCurrentDimensionIndex(); + const auto dimension3 = _dimensionAction3.getCurrentDimensionIndex(); - if (currentDimensionIndex >= 0) - _scatterplotPlugin->loadColors(currentColorDataset.get(), _dimensionAction.getCurrentDimensionIndex()); + if (dimension2 >= 0 && dimension3 >= 0) + _scatterplotPlugin->loadColorsRGB(currentColorDataset.get(), dimension1, dimension2, dimension3); + + break; + } + + default: // Scalar (1D) + _scatterplotPlugin->loadColors(currentColorDataset.get(), dimension1); + break; + } } updateScatterplotWidgetColorMap(); @@ -297,7 +375,16 @@ void ColoringAction::updateScatterplotWidgetColorMap() scatterplotWidget.setColoringMode(ScatterplotWidget::ColoringMode::Scatter); } else { - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + const auto currentColorDataset = getCurrentColorDataset(); + const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; + + if (isDuo) + // mirrored is deprecated in Qt 6.9, flipped can replace it + scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().mirrored(false, true)); + //scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); + else + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + //scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); } break; @@ -324,9 +411,22 @@ void ColoringAction::updateScatterplotWidgetColorMap() void ColoringAction::updateScatterPlotWidgetColorMapRange() { + auto& scatterplotWidget = _scatterplotPlugin->getScatterplotWidget(); + + // The adjustable 1D color-map range only drives the (channel 1) scalar range for 1D scalar coloring. + // In Duo/RGB the color channels each use their own automatically-computed range, so leave channel 1 + // untouched here (otherwise identical channels would normalize differently and produce a color tint). + if (scatterplotWidget.getRenderMode() == ScatterplotWidget::SCATTERPLOT) { + const auto currentColorDataset = getCurrentColorDataset(); + const bool isPointsSource = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType; + + if (isPointsSource && _colorSpaceAction.getCurrentIndex() != 0) // Duo (1) or RGB (2) + return; + } + const auto& rangeAction = _colorMap1DAction.getRangeAction(ColorMapAction::Axis::X); - _scatterplotPlugin->getScatterplotWidget().setColorMapRange(rangeAction.getMinimum(), rangeAction.getMaximum()); + scatterplotWidget.setColorMapRange(rangeAction.getMinimum(), rangeAction.getMaximum()); } bool ColoringAction::shouldEnableColorMap() const @@ -351,9 +451,71 @@ bool ColoringAction::shouldEnableColorMap() const void ColoringAction::updateColorMapActionsReadOnly() { const auto currentIndex = _colorByAction.getCurrentIndex(); + const bool isPointsSource = currentIndex >= 2 && _currentColorPointsDataset.isValid(); + const bool isDuo = isPointsSource && _colorSpaceAction.getCurrentIndex() == 1; + const bool isRGB = isPointsSource && _colorSpaceAction.getCurrentIndex() == 2; + + _colorMap1DAction.setEnabled(shouldEnableColorMap() && (currentIndex >= 2) && !isDuo && !isRGB); + _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1 || isDuo)); +} + +void ColoringAction::updateChannelActionsReadOnly() +{ + const auto currentIndex = _colorByAction.getCurrentIndex(); + const auto colorSpace = _colorSpaceAction.getCurrentIndex(); + + const bool isPointsSource = currentIndex >= 2 && _currentColorPointsDataset.isValid(); + + const bool isDuo = isPointsSource && colorSpace == 1; // Duo (2D) + const bool isRGB = isPointsSource && colorSpace == 2; // RGB + + // All actions remain visible; only their enabled state reflects the current coloring mode. + + // Constant color: only usable in constant mode + _constantColorAction.setEnabled(currentIndex == 0); + + // Color space selector: only usable for a points color source + _colorSpaceAction.setEnabled(isPointsSource); + + // Dimension pickers: channel 1 for any points source, channel 2 for Duo/RGB, channel 3 for RGB only + _dimensionAction.setEnabled(isPointsSource); + _dimensionAction2.setEnabled(isDuo || isRGB); + _dimensionAction3.setEnabled(isRGB); +} + +void ColoringAction::applyDefaultChannels() +{ + if (!_currentColorPointsDataset.isValid()) + return; + + const auto numDimensions = static_cast(_currentColorPointsDataset->getNumDimensions()); - _colorMap1DAction.setEnabled(shouldEnableColorMap() && (currentIndex >= 2)); - _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1)); + switch (_colorSpaceAction.getCurrentIndex()) + { + case 1: // Duo (2D): default to the first two channels + { + if (numDimensions >= 2) { + _dimensionAction.setCurrentDimensionIndex(0); + _dimensionAction2.setCurrentDimensionIndex(1); + } + + break; + } + + case 2: // RGB: default to the first three channels + { + if (numDimensions >= 3) { + _dimensionAction.setCurrentDimensionIndex(0); + _dimensionAction2.setCurrentDimensionIndex(1); + _dimensionAction3.setCurrentDimensionIndex(2); + } + + break; + } + + default: + break; + } } void ColoringAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) @@ -368,7 +530,10 @@ void ColoringAction::connectToPublicAction(WidgetAction* publicAction, bool recu if (recursive) { actions().connectPrivateActionToPublicAction(&_colorByAction, &publicColoringAction->getColorByAction(), recursive); actions().connectPrivateActionToPublicAction(&_constantColorAction, &publicColoringAction->getConstantColorAction(), recursive); + actions().connectPrivateActionToPublicAction(&_colorSpaceAction, &publicColoringAction->getColorSpaceAction(), recursive); actions().connectPrivateActionToPublicAction(&_dimensionAction, &publicColoringAction->getDimensionAction(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionAction2, &publicColoringAction->getDimensionAction2(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionAction3, &publicColoringAction->getDimensionAction3(), recursive); actions().connectPrivateActionToPublicAction(&_colorMap1DAction, &publicColoringAction->getColorMap1DAction(), recursive); actions().connectPrivateActionToPublicAction(&_colorMap2DAction, &publicColoringAction->getColorMap2DAction(), recursive); } @@ -384,7 +549,10 @@ void ColoringAction::disconnectFromPublicAction(bool recursive) if (recursive) { actions().disconnectPrivateActionFromPublicAction(&_colorByAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_constantColorAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_colorSpaceAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_dimensionAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionAction2, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionAction3, recursive); actions().disconnectPrivateActionFromPublicAction(&_colorMap2DAction, recursive); } @@ -395,11 +563,22 @@ void ColoringAction::fromVariantMap(const QVariantMap& variantMap) { GroupAction::fromVariantMap(variantMap); + // Restore the color source first so the dimension pickers are targeted at the right dataset, + // then restore the color space and channels, and finally the color maps. _colorByAction.fromParentVariantMap(variantMap); _constantColorAction.fromParentVariantMap(variantMap); _dimensionAction.fromParentVariantMap(variantMap); + _dimensionAction2.fromParentVariantMap(variantMap); + _dimensionAction3.fromParentVariantMap(variantMap); + _colorSpaceAction.fromParentVariantMap(variantMap); _colorMap1DAction.fromParentVariantMap(variantMap); _colorMap2DAction.fromParentVariantMap(variantMap); + + // Apply the fully-restored coloring state + updateChannelActionsReadOnly(); + updateScatterPlotWidgetColors(); + updateScatterplotWidgetColorMap(); + updateColorMapActionsReadOnly(); } QVariantMap ColoringAction::toVariantMap() const @@ -408,7 +587,10 @@ QVariantMap ColoringAction::toVariantMap() const _colorByAction.insertIntoVariantMap(variantMap); _constantColorAction.insertIntoVariantMap(variantMap); + _colorSpaceAction.insertIntoVariantMap(variantMap); _dimensionAction.insertIntoVariantMap(variantMap); + _dimensionAction2.insertIntoVariantMap(variantMap); + _dimensionAction3.insertIntoVariantMap(variantMap); _colorMap1DAction.insertIntoVariantMap(variantMap); _colorMap2DAction.insertIntoVariantMap(variantMap); diff --git a/src/ColoringAction.h b/src/ColoringAction.h index 4a1d480..a1de452 100644 --- a/src/ColoringAction.h +++ b/src/ColoringAction.h @@ -66,6 +66,12 @@ class ColoringAction : public VerticalGroupAction /** Update the color by action options */ void updateColorByActionOptions(); + /** Enable/disable the color space and channel picker actions for the current coloring mode */ + void updateChannelActionsReadOnly(); + + /** Set the dimension pickers to sensible defaults (the first channels) for the current color space */ + void applyDefaultChannels(); + /** Update the colors of the points in the scatter plot widget */ void updateScatterPlotWidgetColors(); @@ -119,7 +125,10 @@ class ColoringAction : public VerticalGroupAction OptionAction& getColorByAction() { return _colorByAction; } ColorAction& getConstantColorAction() { return _constantColorAction; } + OptionAction& getColorSpaceAction() { return _colorSpaceAction; } DimensionPickerAction& getDimensionAction() { return _dimensionAction; } + DimensionPickerAction& getDimensionAction2() { return _dimensionAction2; } + DimensionPickerAction& getDimensionAction3() { return _dimensionAction3; } ColorMapAction& getColorMap1DAction() { return _colorMap1DAction; } ColorMapAction& getColorMap2DAction() { return _colorMap2DAction; } @@ -131,7 +140,10 @@ class ColoringAction : public VerticalGroupAction ColorSourceModel _colorByModel; /** Color by model (model input for the color by action) */ OptionAction _colorByAction; /** Action for picking the coloring type */ ColorAction _constantColorAction; /** Action for picking the constant color */ - DimensionPickerAction _dimensionAction; /** Dimension picker action */ + OptionAction _colorSpaceAction; /** Color space for data coloring (Scalar 1D / Duo 2D / RGB) */ + DimensionPickerAction _dimensionAction; /** Dimension picker action (color channel 1) */ + DimensionPickerAction _dimensionAction2; /** Dimension picker action (color channel 2, for Duo/RGB) */ + DimensionPickerAction _dimensionAction3; /** Dimension picker action (color channel 3, for RGB) */ ColorMap1DAction _colorMap1DAction; /** One-dimensional color map action */ ColorMap2DAction _colorMap2DAction; /** Two-dimensional color map action */ Dataset _currentColorPointsDataset; /** Current color dataset */ @@ -145,4 +157,4 @@ class ColoringAction : public VerticalGroupAction Q_DECLARE_METATYPE(ColoringAction) -inline const auto coloringActionMetaTypeId = qRegisterMetaType("ColoringAction"); \ No newline at end of file +inline const auto coloringActionMetaTypeId = qRegisterMetaType("ColoringAction"); diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 531eb45..2f18488 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -713,16 +713,16 @@ void ScatterplotPlugin::positionDatasetChanged() updateData(); } -void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std::uint32_t& dimensionIndex) +bool ScatterplotPlugin::mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars) { // Only proceed with valid points dataset if (!pointsColor.isValid()) - return; + return false; const auto numColorPoints = pointsColor->getNumPoints(); // Generate point colorScalars for color mapping - std::vector colorScalars = {}; + colorScalars.clear(); pointsColor->extractDataForDimension(colorScalars, dimensionIndex); // If number of points do not match, use a mapping @@ -815,14 +815,12 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std } catch (const std::exception& e) { - qDebug() << "ScatterplotPlugin::loadColors: mapping failed -> " << e.what(); - _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant - return; + qDebug() << "ScatterplotPlugin::mapColorScalars: mapping failed -> " << e.what(); + return false; } catch (...) { - qDebug() << "ScatterplotPlugin::loadColors: mapping failed for an unknown reason."; - _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant - return; + qDebug() << "ScatterplotPlugin::mapColorScalars: mapping failed for an unknown reason."; + return false; } std::swap(mappedColorScalars, colorScalars); @@ -830,6 +828,18 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std assert(colorScalars.size() == _numPoints); + return true; +} + +void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std::uint32_t& dimensionIndex) +{ + std::vector colorScalars = {}; + + if (!mapColorScalars(pointsColor, dimensionIndex, colorScalars)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + // Assign colorScalars and scalar effect _scatterPlotWidget->setScalars(colorScalars); _scatterPlotWidget->setScalarEffect(PointEffect::Color); @@ -840,6 +850,49 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std getWidget().update(); } +void ScatterplotPlugin::loadColors2D(const Dataset& pointsColor, const std::uint32_t& dimensionIndexX, const std::uint32_t& dimensionIndexY) +{ + std::vector colorScalarsX = {}; + std::vector colorScalarsY = {}; + + if (!mapColorScalars(pointsColor, dimensionIndexX, colorScalarsX) || + !mapColorScalars(pointsColor, dimensionIndexY, colorScalarsY)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + + // Assign both channels and the two-channel 2D coloring effect + _scatterPlotWidget->setScalars(colorScalarsX); + _scatterPlotWidget->setScalars2(colorScalarsY); + _scatterPlotWidget->setScalarEffect(PointEffect::Color2DChannels); + + // Render + getWidget().update(); +} + +void ScatterplotPlugin::loadColorsRGB(const Dataset& pointsColor, const std::uint32_t& dimensionIndexR, const std::uint32_t& dimensionIndexG, const std::uint32_t& dimensionIndexB) +{ + std::vector colorScalarsR = {}; + std::vector colorScalarsG = {}; + std::vector colorScalarsB = {}; + + if (!mapColorScalars(pointsColor, dimensionIndexR, colorScalarsR) || + !mapColorScalars(pointsColor, dimensionIndexG, colorScalarsG) || + !mapColorScalars(pointsColor, dimensionIndexB, colorScalarsB)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + + // Assign the three channels and the RGB coloring effect + _scatterPlotWidget->setScalars(colorScalarsR); + _scatterPlotWidget->setScalars2(colorScalarsG); + _scatterPlotWidget->setScalars3(colorScalarsB); + _scatterPlotWidget->setScalarEffect(PointEffect::ColorRGB); + + // Render + getWidget().update(); +} + void ScatterplotPlugin::loadColors(const Dataset& clusters) { // Only proceed with valid clusters and position dataset diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index 437876b..ef32af8 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -63,6 +63,23 @@ class ScatterplotPlugin : public ViewPlugin */ void loadColors(const Dataset& points, const std::uint32_t& dimensionIndex); + /** + * Load 2D color from two dimensions of a points dataset (mapped through the 2D color map) + * @param points Smart pointer to points dataset + * @param dimensionIndexX Index of the dimension mapped to the color map x-axis + * @param dimensionIndexY Index of the dimension mapped to the color map y-axis + */ + void loadColors2D(const Dataset& points, const std::uint32_t& dimensionIndexX, const std::uint32_t& dimensionIndexY); + + /** + * Load RGB color from three dimensions of a points dataset + * @param points Smart pointer to points dataset + * @param dimensionIndexR Index of the dimension mapped to red + * @param dimensionIndexG Index of the dimension mapped to green + * @param dimensionIndexB Index of the dimension mapped to blue + */ + void loadColorsRGB(const Dataset& points, const std::uint32_t& dimensionIndexR, const std::uint32_t& dimensionIndexG, const std::uint32_t& dimensionIndexB); + /** * Load color from clusters dataset * @param clusters Smart pointer to clusters dataset @@ -113,6 +130,17 @@ class ScatterplotPlugin : public ViewPlugin */ QVariantMap toVariantMap() const override; +private: + + /** + * Extract dimension \p dimensionIndex from \p pointsColor and map it into the position dataset's point space + * @param pointsColor Smart pointer to the color points dataset + * @param dimensionIndex Index of the dimension to extract + * @param colorScalars Output vector of scalars, sized to the number of position points on success + * @return Boolean determining whether the mapping succeeded + */ + bool mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars); + private: mv::gui::DropWidget* _dropWidget; /** Widget for dropping datasets */ ScatterplotWidget* _scatterPlotWidget; /** The visualization widget */ diff --git a/src/ScatterplotWidget.cpp b/src/ScatterplotWidget.cpp index ff6f6f8..837ed7c 100644 --- a/src/ScatterplotWidget.cpp +++ b/src/ScatterplotWidget.cpp @@ -328,7 +328,21 @@ void ScatterplotWidget::setHighlights(const std::vector& highlights, const void ScatterplotWidget::setScalars(const std::vector& scalars) { _pointRenderer.setColorChannelScalars(scalars); - + + update(); +} + +void ScatterplotWidget::setScalars2(const std::vector& scalars) +{ + _pointRenderer.setColorChannel2Scalars(scalars); + + update(); +} + +void ScatterplotWidget::setScalars3(const std::vector& scalars) +{ + _pointRenderer.setColorChannel3Scalars(scalars); + update(); } diff --git a/src/ScatterplotWidget.h b/src/ScatterplotWidget.h index 9e73b49..4441bd6 100644 --- a/src/ScatterplotWidget.h +++ b/src/ScatterplotWidget.h @@ -80,6 +80,12 @@ 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); + /** Set the second color scalar channel (used for 2D and RGB coloring) */ + void setScalars2(const std::vector& scalars); + + /** Set the third color scalar channel (used for RGB coloring) */ + void setScalars3(const std::vector& scalars); + /** * Set colors for each individual data point * @param colors Vector of colors (size must match that of the loaded points dataset) From 5104caf54312f44a44de7e902c4e379351aa3ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20H=C3=B6llt?= Date: Fri, 24 Jul 2026 10:00:32 +0200 Subject: [PATCH 07/17] Fixes Qt 6.10 build Replaced deprecated 'mirrored' method with 'flipped' for color maps. --- src/ColoringAction.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index 71db211..ded2c5c 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -379,12 +379,9 @@ void ColoringAction::updateScatterplotWidgetColorMap() const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; if (isDuo) - // mirrored is deprecated in Qt 6.9, flipped can replace it - scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().mirrored(false, true)); - //scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); + scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); else - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); - //scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); } break; From 7485c00b922b5b68920541f19fc3c60b47c482c2 Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Fri, 24 Jul 2026 10:02:34 +0200 Subject: [PATCH 08/17] CI: Remove Release build and install steps (#248) --- conanfile.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/conanfile.py b/conanfile.py index 73491bf..a8ba72b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -120,7 +120,6 @@ def build(self): cmake = self._configure_cmake() cmake.build(build_type="RelWithDebInfo") - cmake.build(build_type="Release") def package(self): package_dir = pathlib.Path(self.build_folder, "package") @@ -138,23 +137,9 @@ def package(self): relWithDebInfo_dir, ] ) - subprocess.run( - [ - "cmake", - "--install", - self.build_folder, - "--config", - "Release", - "--prefix", - release_dir, - ] - ) self.copy(pattern="*", src=package_dir) def package_info(self): self.cpp_info.relwithdebinfo.libdirs = ["RelWithDebInfo/lib"] self.cpp_info.relwithdebinfo.bindirs = ["RelWithDebInfo/Plugins", "RelWithDebInfo"] self.cpp_info.relwithdebinfo.includedirs = ["RelWithDebInfo/include", "RelWithDebInfo"] - self.cpp_info.release.libdirs = ["Release/lib"] - self.cpp_info.release.bindirs = ["Release/Plugins", "Release"] - self.cpp_info.release.includedirs = ["Release/include", "Release"] From a86c3e03c3cebe9b0b45ab82d654864fe9a3e85a Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Fri, 24 Jul 2026 10:06:15 +0200 Subject: [PATCH 09/17] Upgrade build workflow actions and Python version Updated build workflow to use newer versions of actions and Python. --- .github/workflows/build.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61ed56b..1023720 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,14 +5,10 @@ on: pull_request: workflow_dispatch: -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - # for matrix check https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners jobs: prepare_matrix: - runs-on: ubuntu-latest + runs-on: ubuntu-slim outputs: matrix: ${{ steps.matrix_setup.outputs.matrix }} steps: @@ -34,7 +30,7 @@ jobs: steps: - name: Checkout the source - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive ref: ${{ github.event.pull_request.head.ref }} @@ -45,9 +41,9 @@ jobs: sudo xcode-select -switch /Applications/Xcode_${{matrix.build-xcode-version}}.app - name: Setup python version - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: - python-version: "3.11" + python-version: "3.12" - name: Start ssh key agent uses: webfactory/ssh-agent@v0.9.0 From 73bf4dbcfa855201fd82e18fe2f99309dd050a3e Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Tue, 28 Jul 2026 08:59:21 +0200 Subject: [PATCH 10/17] Revert principal dimension action name change (#250) * Revert principle dimension action name change * Ignore loading errors for newly introduced actions Do this for backwards compatibility --- src/ColoringAction.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index ded2c5c..a0859e1 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -17,7 +17,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : _colorByAction(this, "Color by"), _constantColorAction(this, "Constant color", DEFAULT_CONSTANT_COLOR), _colorSpaceAction(this, "Color space", { "Scalar (1D)", "Duo (2D)", "RGB" }, "Scalar (1D)"), - _dimensionAction(this, "Dimension 1"), + _dimensionAction(this, "Dimension"), _dimensionAction2(this, "Dimension 2"), _dimensionAction3(this, "Dimension 3"), _colorMap1DAction(this, "1D Color map"), @@ -565,9 +565,9 @@ void ColoringAction::fromVariantMap(const QVariantMap& variantMap) _colorByAction.fromParentVariantMap(variantMap); _constantColorAction.fromParentVariantMap(variantMap); _dimensionAction.fromParentVariantMap(variantMap); - _dimensionAction2.fromParentVariantMap(variantMap); - _dimensionAction3.fromParentVariantMap(variantMap); - _colorSpaceAction.fromParentVariantMap(variantMap); + _dimensionAction2.fromParentVariantMap(variantMap, true); + _dimensionAction3.fromParentVariantMap(variantMap, true); + _colorSpaceAction.fromParentVariantMap(variantMap, true); _colorMap1DAction.fromParentVariantMap(variantMap); _colorMap2DAction.fromParentVariantMap(variantMap); From a3db8a66cfe2a504a3125b59b5b8b09e6c7f61e4 Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Mon, 17 Aug 2026 14:37:07 +0200 Subject: [PATCH 11/17] Remove restrictive condition --- src/ScatterplotPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 2f18488..4613951 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -927,7 +927,7 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) } } - else if(globalIndices.size() == _numPoints) + else { // Loop over all clusters and populate global colors for (const auto& cluster : clusterVec) From aac463035b0224a34fe7fa5d9363f06bee7ef66e Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Mon, 17 Aug 2026 14:37:29 +0200 Subject: [PATCH 12/17] Use range for, eliminates index --- src/ScatterplotPlugin.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 4613951..d3a9e3a 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -918,11 +918,10 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) if (totalNumPoints == _numPoints && clusterVec.size() == totalNumPoints) { - for (size_t i = 0; i < static_cast(clusterVec.size()); i++) + // Each cluster corresponds to one point + for (const auto& cluster : clusterVec) { - const auto& cluster = clusterVec[i]; const auto color = cluster.getColor(); - localColors[cluster.getIndices()[0]] = Vector3f(color.redF(), color.greenF(), color.blueF()); } From 3dc0df5e5f4fcf8c8106fbf2368695f65a7cb4a7 Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Mon, 17 Aug 2026 14:37:59 +0200 Subject: [PATCH 13/17] Track totalPoints class wide --- src/ScatterplotPlugin.cpp | 16 +++++++--------- src/ScatterplotPlugin.h | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index d3a9e3a..bc52e18 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -51,6 +51,7 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : _dropWidget(nullptr), _scatterPlotWidget(new ScatterplotWidget(this)), _numPoints(0), + _numTotalPoints(0), _settingsAction(new SettingsAction(this, "Settings")), _primaryToolbarAction(new HorizontalToolbarAction(this, "Primary Toolbar")) { @@ -707,6 +708,10 @@ void ScatterplotPlugin::positionDatasetChanged() _numPoints = _positionDataset->getNumPoints(); + _numTotalPoints = _positionDataset->isDerivedData() + ? _positionSourceDataset->getFullDataset()->getNumPoints() + : _positionDataset->getFullDataset()->getNumPoints(); + _scatterPlotWidget->getPointRendererNavigator().resetView(true); _scatterPlotWidget->getDensityRendererNavigator().resetView(true); @@ -899,24 +904,17 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) if (!clusters.isValid() || !_positionDataset.isValid()) return; - // Get global indices from the position dataset - std::uint64_t totalNumPoints = 0; - if (_positionDataset->isDerivedData()) - totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); - else - totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); - // Mapping from local to global indices std::vector globalIndices; _positionDataset->getGlobalIndices(globalIndices); // Generate color buffer for global and local colors - std::vector globalColors(totalNumPoints); + std::vector globalColors(_numTotalPoints); std::vector localColors(_numPoints); const auto& clusterVec = clusters->getClusters(); - if (totalNumPoints == _numPoints && clusterVec.size() == totalNumPoints) + if (_numTotalPoints == _numPoints && static_cast(clusterVec.size()) == _numTotalPoints) { // Each cluster corresponds to one point for (const auto& cluster : clusterVec) diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index ef32af8..c8e2b42 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -148,6 +148,7 @@ class ScatterplotPlugin : public ViewPlugin Dataset _positionSourceDataset; /** Smart pointer to source of the points dataset for point position (if any) */ std::vector _positions; /** Point positions */ std::uint64_t _numPoints; /** Number of point positions */ + std::uint64_t _numTotalPoints; /** Number of points in positions data set (might be more than _numPoints) */ QPointer _settingsAction; /** Group action for all settings */ QPointer _primaryToolbarAction; /** Horizontal toolbar for primary content */ QRectF _selectionBoundaries; /** Boundaries of the selection */ From 0a173143c72853a549cf59b3407c3bbd424c1b89 Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Mon, 17 Aug 2026 14:38:29 +0200 Subject: [PATCH 14/17] Check if cluster indices exceed point indices instead of checking of they provide full coverage --- src/ScatterplotPlugin.cpp | 64 +++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index bc52e18..6bde5ea 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -34,10 +34,28 @@ #include #include #include +#include #include #include #include +#ifdef __cpp_lib_execution +#ifdef __GNUC__ // both TBB and Qt define emit keyword: undef +#undef emit +#endif +#include +#ifdef __GNUC__ // both TBB and Qt define emit keyword: def again +#define emit +#endif +#ifdef NDEBUG +#define MV_SCATTER_PARALLEL_EXECUTION std::execution::par, +#else +#define MV_SCATTER_PARALLEL_EXECUTION std::execution::seq, +#endif +#else +#define MV_SCATTER_PARALLEL_EXECUTION +#endif + #define VIEW_SAMPLING_HTML //#define VIEW_SAMPLING_WIDGET @@ -246,19 +264,39 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : { // Check to set whether the number of data points comprised throughout all clusters is the same number // as the number of data points in the dataset we are trying to color - std::uint64_t totalNumIndices = 0; - for (const Cluster& cluster : candidateDataset->getClusters()) - { - totalNumIndices += cluster.getIndices().size(); - } - - std::uint64_t totalNumPoints = 0; - if (_positionDataset->isDerivedData()) - totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); - else - totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); - - if (totalNumIndices == totalNumPoints) + //std::uint64_t totalNumIndices = 0; + //for (const Cluster& cluster : candidateDataset->getClusters()) + //{ + // totalNumIndices += cluster.getIndices().size(); + //} + + auto getMaxIndex = [](const QVector& clusters) -> std::uint32_t + { + if (clusters.empty()) + return std::numeric_limits::lowest(); + + std::vector clusterIndicesMax(clusters.size()); + + std::transform( + MV_SCATTER_PARALLEL_EXECUTION + clusters.cbegin(), clusters.cend(), + clusterIndicesMax.begin(), + [](const Cluster& cluster) -> std::uint32_t { + const std::vector& indices = cluster.getIndices(); + if (indices.empty()) + return std::numeric_limits::lowest(); + + return *std::ranges::max_element(indices); + }); + + return *std::max_element( + MV_SCATTER_PARALLEL_EXECUTION + clusterIndicesMax.cbegin(), clusterIndicesMax.cend()); + }; + + const auto maxIndex = getMaxIndex(candidateDataset->getClusters()); + + if (maxIndex < _numTotalPoints) { // Use the clusters set for points color dropRegions << new DropWidget::DropRegion(this, "Color", description, "palette", true, [this, candidateDataset]() { From 7c4e5a83cece36697b10362c9e925d5723fdff95 Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Mon, 17 Aug 2026 14:41:22 +0200 Subject: [PATCH 15/17] Revert last 4 commits --- src/ScatterplotPlugin.cpp | 87 ++++++++++++--------------------------- src/ScatterplotPlugin.h | 1 - 2 files changed, 26 insertions(+), 62 deletions(-) diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 6bde5ea..2f18488 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -34,28 +34,10 @@ #include #include #include -#include #include #include #include -#ifdef __cpp_lib_execution -#ifdef __GNUC__ // both TBB and Qt define emit keyword: undef -#undef emit -#endif -#include -#ifdef __GNUC__ // both TBB and Qt define emit keyword: def again -#define emit -#endif -#ifdef NDEBUG -#define MV_SCATTER_PARALLEL_EXECUTION std::execution::par, -#else -#define MV_SCATTER_PARALLEL_EXECUTION std::execution::seq, -#endif -#else -#define MV_SCATTER_PARALLEL_EXECUTION -#endif - #define VIEW_SAMPLING_HTML //#define VIEW_SAMPLING_WIDGET @@ -69,7 +51,6 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : _dropWidget(nullptr), _scatterPlotWidget(new ScatterplotWidget(this)), _numPoints(0), - _numTotalPoints(0), _settingsAction(new SettingsAction(this, "Settings")), _primaryToolbarAction(new HorizontalToolbarAction(this, "Primary Toolbar")) { @@ -264,39 +245,19 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : { // Check to set whether the number of data points comprised throughout all clusters is the same number // as the number of data points in the dataset we are trying to color - //std::uint64_t totalNumIndices = 0; - //for (const Cluster& cluster : candidateDataset->getClusters()) - //{ - // totalNumIndices += cluster.getIndices().size(); - //} - - auto getMaxIndex = [](const QVector& clusters) -> std::uint32_t - { - if (clusters.empty()) - return std::numeric_limits::lowest(); - - std::vector clusterIndicesMax(clusters.size()); - - std::transform( - MV_SCATTER_PARALLEL_EXECUTION - clusters.cbegin(), clusters.cend(), - clusterIndicesMax.begin(), - [](const Cluster& cluster) -> std::uint32_t { - const std::vector& indices = cluster.getIndices(); - if (indices.empty()) - return std::numeric_limits::lowest(); - - return *std::ranges::max_element(indices); - }); - - return *std::max_element( - MV_SCATTER_PARALLEL_EXECUTION - clusterIndicesMax.cbegin(), clusterIndicesMax.cend()); - }; - - const auto maxIndex = getMaxIndex(candidateDataset->getClusters()); - - if (maxIndex < _numTotalPoints) + std::uint64_t totalNumIndices = 0; + for (const Cluster& cluster : candidateDataset->getClusters()) + { + totalNumIndices += cluster.getIndices().size(); + } + + std::uint64_t totalNumPoints = 0; + if (_positionDataset->isDerivedData()) + totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); + else + totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); + + if (totalNumIndices == totalNumPoints) { // Use the clusters set for points color dropRegions << new DropWidget::DropRegion(this, "Color", description, "palette", true, [this, candidateDataset]() { @@ -746,10 +707,6 @@ void ScatterplotPlugin::positionDatasetChanged() _numPoints = _positionDataset->getNumPoints(); - _numTotalPoints = _positionDataset->isDerivedData() - ? _positionSourceDataset->getFullDataset()->getNumPoints() - : _positionDataset->getFullDataset()->getNumPoints(); - _scatterPlotWidget->getPointRendererNavigator().resetView(true); _scatterPlotWidget->getDensityRendererNavigator().resetView(true); @@ -942,27 +899,35 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) if (!clusters.isValid() || !_positionDataset.isValid()) return; + // Get global indices from the position dataset + std::uint64_t totalNumPoints = 0; + if (_positionDataset->isDerivedData()) + totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); + else + totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); + // Mapping from local to global indices std::vector globalIndices; _positionDataset->getGlobalIndices(globalIndices); // Generate color buffer for global and local colors - std::vector globalColors(_numTotalPoints); + std::vector globalColors(totalNumPoints); std::vector localColors(_numPoints); const auto& clusterVec = clusters->getClusters(); - if (_numTotalPoints == _numPoints && static_cast(clusterVec.size()) == _numTotalPoints) + if (totalNumPoints == _numPoints && clusterVec.size() == totalNumPoints) { - // Each cluster corresponds to one point - for (const auto& cluster : clusterVec) + for (size_t i = 0; i < static_cast(clusterVec.size()); i++) { + const auto& cluster = clusterVec[i]; const auto color = cluster.getColor(); + localColors[cluster.getIndices()[0]] = Vector3f(color.redF(), color.greenF(), color.blueF()); } } - else + else if(globalIndices.size() == _numPoints) { // Loop over all clusters and populate global colors for (const auto& cluster : clusterVec) diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index c8e2b42..ef32af8 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -148,7 +148,6 @@ class ScatterplotPlugin : public ViewPlugin Dataset _positionSourceDataset; /** Smart pointer to source of the points dataset for point position (if any) */ std::vector _positions; /** Point positions */ std::uint64_t _numPoints; /** Number of point positions */ - std::uint64_t _numTotalPoints; /** Number of points in positions data set (might be more than _numPoints) */ QPointer _settingsAction; /** Group action for all settings */ QPointer _primaryToolbarAction; /** Horizontal toolbar for primary content */ QRectF _selectionBoundaries; /** Boundaries of the selection */ From 81b78ead48f1a3ad7a8efdeb94b8a0ca23dc7176 Mon Sep 17 00:00:00 2001 From: Alexander Vieth Date: Tue, 18 Aug 2026 13:07:23 +0200 Subject: [PATCH 16/17] Accept more valid clusters for coloring (#253) * Remove restrictive condition * Use range for, eliminates index * Check if cluster indices exceed point indices instead of checking of they provide full coverage * link against tbb with gcc --- CMakeLists.txt | 8 ++++ conanfile.py | 5 ++- src/ScatterplotPlugin.cpp | 93 ++++++++++++++++++++++++++------------- src/ScatterplotPlugin.h | 6 +++ 4 files changed, 79 insertions(+), 33 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 39453a5..f3fc5aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,10 @@ find_package(Qt6 COMPONENTS Widgets WebEngineWidgets OpenGL OpenGLWidgets REQUIR find_package(ManiVault COMPONENTS Core PointData ClusterData ColorData ImageData CONFIG QUIET) mv_project_defaults() +if(UNIX AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + find_package(TBB REQUIRED) +endif() + # ----------------------------------------------------------------------------- # Source files # ----------------------------------------------------------------------------- @@ -126,6 +130,10 @@ target_link_libraries(${PROJECT} PRIVATE ManiVault::ClusterData) target_link_libraries(${PROJECT} PRIVATE ManiVault::ImageData) target_link_libraries(${PROJECT} PRIVATE ManiVault::ColorData) +if(UNIX AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_link_libraries(${PROJECT} PRIVATE TBB::tbb) +endif() + # ----------------------------------------------------------------------------- # Target installation # ----------------------------------------------------------------------------- diff --git a/conanfile.py b/conanfile.py index a8ba72b..1980dd0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,6 +1,7 @@ from conans import ConanFile from conan.tools.cmake import CMakeDeps, CMake, CMakeToolchain from conans.tools import save, load +from conans.tools import os_info from conans import tools import os import pathlib @@ -72,8 +73,8 @@ def configure(self): pass def system_requirements(self): - # May be needed for macOS or Linux - pass + if os_info.is_linux: + self.run("sudo apt update && sudo apt install -y libtbb-dev") def config_options(self): if self.settings.os == "Windows": diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 2f18488..41383a4 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -34,10 +34,28 @@ #include #include #include +#include #include #include #include +#ifdef __cpp_lib_execution +#ifdef __GNUC__ // both TBB and Qt define emit keyword: undef +#undef emit +#endif +#include +#ifdef __GNUC__ // both TBB and Qt define emit keyword: def again +#define emit +#endif +#ifdef NDEBUG +#define MV_SCATTER_PARALLEL_EXECUTION std::execution::par, +#else +#define MV_SCATTER_PARALLEL_EXECUTION std::execution::seq, +#endif +#else +#define MV_SCATTER_PARALLEL_EXECUTION +#endif + #define VIEW_SAMPLING_HTML //#define VIEW_SAMPLING_WIDGET @@ -243,21 +261,34 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : else { if (candidateDataset.isValid()) { - // Check to set whether the number of data points comprised throughout all clusters is the same number - // as the number of data points in the dataset we are trying to color - std::uint64_t totalNumIndices = 0; - for (const Cluster& cluster : candidateDataset->getClusters()) - { - totalNumIndices += cluster.getIndices().size(); - } - - std::uint64_t totalNumPoints = 0; - if (_positionDataset->isDerivedData()) - totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); - else - totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); - - if (totalNumIndices == totalNumPoints) + // Check that the max index in the cluster data does not exceed the max index of the shown point data + auto getMaxIndex = [](const QVector& clusters) -> std::uint32_t + { + if (clusters.empty()) + return std::numeric_limits::lowest(); + + std::vector clusterIndicesMax(clusters.size()); + + std::transform( + MV_SCATTER_PARALLEL_EXECUTION + clusters.cbegin(), clusters.cend(), + clusterIndicesMax.begin(), + [](const Cluster& cluster) -> std::uint32_t { + const std::vector& indices = cluster.getIndices(); + if (indices.empty()) + return std::numeric_limits::lowest(); + + return *std::ranges::max_element(indices); + }); + + return *std::max_element( + MV_SCATTER_PARALLEL_EXECUTION + clusterIndicesMax.cbegin(), clusterIndicesMax.cend()); + }; + + const auto maxIndex = getMaxIndex(candidateDataset->getClusters()); + + if (maxIndex < numTotalPoints()) { // Use the clusters set for points color dropRegions << new DropWidget::DropRegion(this, "Color", description, "palette", true, [this, candidateDataset]() { @@ -698,13 +729,8 @@ void ScatterplotPlugin::positionDatasetChanged() if (!_positionDataset.isValid()) return; - // Reset dataset references - //_positionSourceDataset.reset(); - - // Set position source dataset reference when the position dataset is derived - //if (_positionDataset->isDerivedData()) _positionSourceDataset = _positionDataset->getSourceDataset(); - + _numPoints = _positionDataset->getNumPoints(); _scatterPlotWidget->getPointRendererNavigator().resetView(true); @@ -713,6 +739,16 @@ void ScatterplotPlugin::positionDatasetChanged() updateData(); } +std::uint64_t ScatterplotPlugin::numTotalPoints() const +{ + if (!_positionDataset.isValid()) + return 0; + + return _positionDataset->isDerivedData() + ? _positionSourceDataset->getFullDataset()->getNumPoints() + : _positionDataset->getFullDataset()->getNumPoints(); +} + bool ScatterplotPlugin::mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars) { // Only proceed with valid points dataset @@ -900,11 +936,7 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) return; // Get global indices from the position dataset - std::uint64_t totalNumPoints = 0; - if (_positionDataset->isDerivedData()) - totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); - else - totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); + const std::uint64_t totalNumPoints = numTotalPoints(); // Mapping from local to global indices std::vector globalIndices; @@ -916,18 +948,17 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) const auto& clusterVec = clusters->getClusters(); - if (totalNumPoints == _numPoints && clusterVec.size() == totalNumPoints) + if (totalNumPoints == _numPoints && static_cast(clusterVec.size()) == totalNumPoints) { - for (size_t i = 0; i < static_cast(clusterVec.size()); i++) + // Each cluster corresponds to one point + for (const auto& cluster : clusterVec) { - const auto& cluster = clusterVec[i]; const auto color = cluster.getColor(); - localColors[cluster.getIndices()[0]] = Vector3f(color.redF(), color.greenF(), color.blueF()); } } - else if(globalIndices.size() == _numPoints) + else { // Loop over all clusters and populate global colors for (const auto& cluster : clusterVec) diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index ef32af8..c04e6a8 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -141,6 +141,12 @@ class ScatterplotPlugin : public ViewPlugin */ bool mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars); + /** + * Number of points in positions data set (might be more than _numPoints) + * @return Number of points in positions data set (might be more than _numPoints) + */ + std::uint64_t numTotalPoints() const; + private: mv::gui::DropWidget* _dropWidget; /** Widget for dropping datasets */ ScatterplotWidget* _scatterPlotWidget; /** The visualization widget */ From fd6db0ce93c69224cf380da2b818415b3a6425be Mon Sep 17 00:00:00 2001 From: Thomas Kroes Date: Thu, 10 Sep 2026 09:51:06 +0200 Subject: [PATCH 17/17] Add Z-order controls and dimension-based selection restrictions (#254) * 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. * 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. * 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. * 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. --- CMakeLists.txt | 4 + src/MiscellaneousAction.cpp | 17 +- src/MiscellaneousAction.h | 5 +- src/ScatterplotPlugin.cpp | 103 +++++++++- src/ScatterplotPlugin.h | 6 +- src/ScatterplotWidget.cpp | 102 +++++++++- src/ScatterplotWidget.h | 35 +++- src/SelectionAction.cpp | 16 +- src/SelectionAction.h | 6 +- src/SelectionRestrictionAction.cpp | 248 +++++++++++++++++++++++ src/SelectionRestrictionAction.h | 54 +++++ src/SettingsAction.cpp | 26 ++- src/SettingsAction.h | 5 +- src/ZOrderingAction.cpp | 308 +++++++++++++++++++++++++++++ src/ZOrderingAction.h | 59 ++++++ 15 files changed, 953 insertions(+), 41 deletions(-) create mode 100644 src/SelectionRestrictionAction.cpp create mode 100644 src/SelectionRestrictionAction.h create mode 100644 src/ZOrderingAction.cpp create mode 100644 src/ZOrderingAction.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f3fc5aa..8db9c9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,10 +67,14 @@ 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 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 a6808a6..7a6ee52 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -117,6 +117,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()); @@ -499,6 +500,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()) @@ -536,6 +609,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 @@ -610,6 +686,8 @@ void ScatterplotPlugin::selectPoints() } } + filterSelectionExcludedIndices(targetSelectionIndices); + auto& navigationAction = navigator.getNavigationAction(); navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && navigationAction.isNavigationActive()); @@ -650,6 +728,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()); @@ -1069,6 +1150,7 @@ void ScatterplotPlugin::updateData() // Pass the 2D points to the scatter plot widget _scatterPlotWidget->setData(&_positions); + _settingsAction->getZOrderingAction().updateScatterplotWidget(); updateSelection(); } @@ -1076,6 +1158,7 @@ void ScatterplotPlugin::updateData() _numPoints = 0; _positions.clear(); _scatterPlotWidget->setData(&_positions); + _settingsAction->getZOrderingAction().updateScatterplotWidget(); } } @@ -1109,14 +1192,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)); @@ -1152,6 +1237,8 @@ void ScatterplotPlugin::updateSelection() { "RenderMode", _settingsAction->getRenderModeAction().getCurrentText() } }); } + + updateHeadsUpDisplay(); } void ScatterplotPlugin::updateHeadsUpDisplay() @@ -1181,6 +1268,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 c04e6a8..fda1451 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -44,6 +44,9 @@ 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); @@ -111,6 +114,7 @@ class ScatterplotPlugin : public ViewPlugin void updateData(); void updateSelection(); void updateHeadsUpDisplayTextColor(); + void filterSelectionExcludedIndices(std::vector& globalIndices) const; public: @@ -190,4 +194,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..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); @@ -613,6 +690,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 +775,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..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); @@ -104,6 +115,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 +221,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 @@ -295,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 */ @@ -311,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..72b6aa8 100644 --- a/src/SelectionAction.cpp +++ b/src/SelectionAction.cpp @@ -15,7 +15,8 @@ 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"), + _selectionRestrictionAction(this, "Selection restriction") { setIconByName("mouse-pointer"); @@ -36,6 +37,7 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : addAction(&getOutlineOpacityAction()); addAction(&getOutlineHaloEnabledAction()); addAction(&getFreezeSelectionAction()); + addAction(&_selectionRestrictionAction); _pixelSelectionAction.getOverlayColorAction().setText("Color"); @@ -88,9 +90,11 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) _outlineHaloEnabledAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineHaloEnabled()); _outlineOverrideColorAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineOverrideColor()); + _selectionRestrictionAction.initialize(scatterplotPlugin); + 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 +104,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 +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(&_selectionRestrictionAction, &publicSelectionAction->getSelectionRestrictionAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -171,6 +176,7 @@ void SelectionAction::disconnectFromPublicAction(bool recursive) actions().disconnectPrivateActionFromPublicAction(&_outlineOpacityAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_outlineHaloEnabledAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_freezeSelectionAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_selectionRestrictionAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); @@ -188,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 @@ -202,6 +209,7 @@ QVariantMap SelectionAction::toVariantMap() const _outlineOpacityAction.insertIntoVariantMap(variantMap); _outlineHaloEnabledAction.insertIntoVariantMap(variantMap); _freezeSelectionAction.insertIntoVariantMap(variantMap); + _selectionRestrictionAction.insertIntoVariantMap(variantMap); return variantMap; -} \ No newline at end of file +} diff --git a/src/SelectionAction.h b/src/SelectionAction.h index d564c5e..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,6 +67,7 @@ class SelectionAction : public GroupAction DecimalAction& getOutlineOpacityAction() { return _outlineOpacityAction; } ToggleAction& getOutlineHaloEnabledAction() { return _outlineHaloEnabledAction; } ToggleAction& getFreezeSelectionAction() { return _freezeSelectionAction; } + SelectionRestrictionAction& getSelectionRestrictionAction() { return _selectionRestrictionAction; } private: PixelSelectionAction _pixelSelectionAction; /** Pixel selection action */ @@ -75,10 +78,11 @@ class SelectionAction : public GroupAction DecimalAction _outlineOpacityAction; /** Selection outline opacity action */ ToggleAction _outlineHaloEnabledAction; /** Selection outline halo enabled action */ ToggleAction _freezeSelectionAction; /** Freeze selection action */ + SelectionRestrictionAction _selectionRestrictionAction; /** Dimension-based selection restriction */ 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/SelectionRestrictionAction.cpp b/src/SelectionRestrictionAction.cpp new file mode 100644 index 0000000..0d78f00 --- /dev/null +++ b/src/SelectionRestrictionAction.cpp @@ -0,0 +1,248 @@ +#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); + setShowLabels(false); + + 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 f0739dc..c446a1b 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -15,11 +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") @@ -27,9 +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]() { @@ -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,16 +70,31 @@ 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); + _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. + 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..f066cd7 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,11 +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 new file mode 100644 index 0000000..60acfd7 --- /dev/null +++ b/src/ZOrderingAction.cpp @@ -0,0 +1,308 @@ +#include "ZOrderingAction.h" + +#include "ScatterplotPlugin.h" +#include "ScatterplotWidget.h" +#include "SelectionAction.h" +#include "SelectionRestrictionAction.h" +#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"), + _restrictSelectionByZOrderAction(this, "Restrict selection by Z-order dimension", false) +{ + setIconByName("sort"); + setLabelSizingType(LabelSizingType::Auto); + setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + + addAction(&_modeAction, OptionAction::HorizontalButtons); + addAction(&_dimensionPickerAction); + 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"); + _restrictSelectionByZOrderAction.setToolTip("Restrict selection using the current Z-order dimension and the selectable range below"); + _dimensionPickerAction.setEnabled(false); + _restrictSelectionByZOrderAction.setEnabled(false); +} + +void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) +{ + Q_ASSERT(scatterplotPlugin != nullptr); + + if (scatterplotPlugin == nullptr) + return; + + _scatterplotPlugin = scatterplotPlugin; + + auto& selectionRestrictionAction = dynamic_cast(parent())->getSelectionAction().getSelectionRestrictionAction(); + + const auto updateDataset = [this]() { + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + + if (!positionDataset.isValid()) { + _dimensionPickerAction.setPointsDataset(Dataset()); + _zOrderScalars.clear(); + 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); + + updateZOrderScalars(); + updateScatterplotWidget(); + }; + + connect(&_modeAction, &OptionAction::currentIndexChanged, this, [this]() { + updateScatterplotWidget(); + }); + + connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this, selectionRestriction = &selectionRestrictionAction]() { + updateZOrderScalars(); + + if (_restrictSelectionByZOrderAction.isChecked() && !_updatingCoupledRestriction) { + _updatingCoupledRestriction = true; + { + selectionRestriction->getDimensionPickerAction().setCurrentDimensionIndex(_dimensionPickerAction.getCurrentDimensionIndex()); + } + _updatingCoupledRestriction = false; + } + + updateScatterplotWidget(); + }); + + 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); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataDimensionsChanged, this, updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataChanged, this, [this]() { + updateZOrderScalars(); + 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); + _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: + _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) + updateZOrderScalars(); +} + +void ZOrderingAction::updateZOrderScalars() +{ + _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); +} + +QMenu* ZOrderingAction::getContextMenu(QWidget* parent) +{ + auto menu = new QMenu("Z ordering", parent); + + menu->addAction(&_modeAction); + menu->addAction(&_dimensionPickerAction); + + if (_scatterplotPlugin != nullptr) { + auto& selectionRestriction = dynamic_cast(this->parent())->getSelectionAction().getSelectionRestrictionAction(); + + menu->addSeparator(); + menu->addAction(&_restrictSelectionByZOrderAction); + menu->addAction(&selectionRestriction.getRangeAction()); + } + + 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); + actions().connectPrivateActionToPublicAction(&_restrictSelectionByZOrderAction, &publicZOrderingAction->getRestrictSelectionByZOrderAction(), recursive); + } + + GroupAction::connectToPublicAction(publicAction, recursive); +} + +void ZOrderingAction::disconnectFromPublicAction(bool recursive) +{ + if (!isConnected()) + return; + + if (recursive) { + actions().disconnectPrivateActionFromPublicAction(&_modeAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_restrictSelectionByZOrderAction, 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..2beadd2 --- /dev/null +++ b/src/ZOrderingAction.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#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; } + ToggleAction& getRestrictSelectionByZOrderAction() { return _restrictSelectionByZOrderAction; } + +private: + void updateZOrderScalars(); + + ScatterplotPlugin* _scatterplotPlugin = nullptr; + OptionAction _modeAction; + DimensionPickerAction _dimensionPickerAction; + ToggleAction _restrictSelectionByZOrderAction; + std::vector _zOrderScalars; + bool _updatingCoupledRestriction = false; + + friend class mv::AbstractActionsManager; +}; + +Q_DECLARE_METATYPE(ZOrderingAction) + +inline const auto zOrderingActionMetaTypeId = qRegisterMetaType("ZOrderingAction");