diff --git a/ManiVault/cmake/CMakeMvSourcesPublic.cmake b/ManiVault/cmake/CMakeMvSourcesPublic.cmake index 12d6a3ce8..5b19041f6 100644 --- a/ManiVault/cmake/CMakeMvSourcesPublic.cmake +++ b/ManiVault/cmake/CMakeMvSourcesPublic.cmake @@ -93,6 +93,12 @@ set(PUBLIC_NUMERICAL_ACTIONS_HEADERS src/actions/NumericalRangeAction.h src/actions/DecimalRangeAction.h src/actions/IntegralRangeAction.h + src/actions/NumericalPointAction.h + src/actions/DecimalPointAction.h + src/actions/IntegralPointAction.h + src/actions/RectangleAction.h + src/actions/IntegralRectangleAction.h + src/actions/DecimalRectangleAction.h ) set(PUBLIC_NUMERICAL_ACTIONS_SOURCES @@ -102,6 +108,12 @@ set(PUBLIC_NUMERICAL_ACTIONS_SOURCES src/actions/NumericalRangeAction.cpp src/actions/DecimalRangeAction.cpp src/actions/IntegralRangeAction.cpp + src/actions/NumericalPointAction.cpp + src/actions/DecimalPointAction.cpp + src/actions/IntegralPointAction.cpp + src/actions/RectangleAction.cpp + src/actions/IntegralRectangleAction.cpp + src/actions/DecimalRectangleAction.cpp ) set(PUBLIC_NUMERICAL_ACTIONS_FILES @@ -109,23 +121,6 @@ set(PUBLIC_NUMERICAL_ACTIONS_FILES ${PUBLIC_NUMERICAL_ACTIONS_SOURCES} ) -set(PUBLIC_RECTANGLE_ACTIONS_HEADERS - src/actions/RectangleAction.h - src/actions/IntegralRectangleAction.h - src/actions/DecimalRectangleAction.h -) - -set(PUBLIC_RECTANGLE_ACTIONS_SOURCES - src/actions/RectangleAction.cpp - src/actions/IntegralRectangleAction.cpp - src/actions/DecimalRectangleAction.cpp -) - -set(PUBLIC_RECTANGLE_ACTIONS_FILES - ${PUBLIC_RECTANGLE_ACTIONS_HEADERS} - ${PUBLIC_RECTANGLE_ACTIONS_SOURCES} -) - set(PUBLIC_TEXTUAL_ACTIONS_HEADERS src/actions/StringAction.h src/actions/StringsAction.h @@ -355,6 +350,7 @@ set(PUBLIC_ACTIONS_INTERNAL_HEADERS src/actions/PaletteColorRoleAction.h src/actions/ColorSchemeAction.h src/actions/EditColorSchemeAction.h + src/actions/NavigationAction.h ) set(PUBLIC_ACTIONS_INTERNAL_SOURCES @@ -393,6 +389,7 @@ set(PUBLIC_ACTIONS_INTERNAL_SOURCES src/actions/PaletteColorRoleAction.cpp src/actions/ColorSchemeAction.cpp src/actions/EditColorSchemeAction.cpp + src/actions/NavigationAction.cpp ) set(PUBLIC_ACTIONS_INTERNAL_FILES @@ -472,13 +469,15 @@ set(PUBLIC_RENDERERS_HEADERS src/renderers/Renderer.h src/renderers/PointRenderer.h src/renderers/DensityRenderer.h - src/renderers/ImageRenderer.h + src/renderers/Renderer2D.h + src/renderers/Navigator2D.h ) set(PUBLIC_RENDERERS_SOURCES src/renderers/PointRenderer.cpp src/renderers/DensityRenderer.cpp - src/renderers/ImageRenderer.cpp + src/renderers/Renderer2D.cpp + src/renderers/Navigator2D.cpp ) set(PUBLIC_RENDERERS_FILES @@ -1069,7 +1068,6 @@ set(PUBLIC_HEADERS ${PUBLIC_EVENT_HEADERS} ${PUBLIC_COLOR_MAP_ACTION_HEADERS} ${PUBLIC_NUMERICAL_ACTIONS_HEADERS} - ${PUBLIC_RECTANGLE_ACTIONS_HEADERS} ${PUBLIC_TEXTUAL_ACTIONS_HEADERS} ${PUBLIC_GROUPING_ACTIONS_HEADERS} ${PUBLIC_TRIGGER_ACTIONS_HEADERS} @@ -1118,7 +1116,6 @@ set(PUBLIC_SOURCES ${PUBLIC_EVENT_SOURCES} ${PUBLIC_COLOR_MAP_ACTION_SOURCES} ${PUBLIC_NUMERICAL_ACTIONS_SOURCES} - ${PUBLIC_RECTANGLE_ACTIONS_SOURCES} ${PUBLIC_TEXTUAL_ACTIONS_SOURCES} ${PUBLIC_GROUPING_ACTIONS_SOURCES} ${PUBLIC_TRIGGER_ACTIONS_SOURCES} @@ -1180,7 +1177,6 @@ source_group(CoreInterface FILES ${PUBLIC_CORE_INTERFACE_FILES}) source_group(Event FILES ${PUBLIC_EVENT_FILES}) source_group(Actions\\Colormap FILES ${PUBLIC_COLOR_MAP_ACTION_FILES}) source_group(Actions\\Numerical FILES ${PUBLIC_NUMERICAL_ACTIONS_FILES}) -source_group(Actions\\Rectangle FILES ${PUBLIC_RECTANGLE_ACTIONS_FILES}) source_group(Actions\\Textual FILES ${PUBLIC_TEXTUAL_ACTIONS_FILES}) source_group(Actions\\Grouping FILES ${PUBLIC_GROUPING_ACTIONS_FILES}) source_group(Actions\\Trigger FILES ${PUBLIC_TRIGGER_ACTIONS_FILES}) diff --git a/ManiVault/res/shaders/DensityDraw.frag b/ManiVault/res/shaders/DensityDraw.frag index d21a90ea8..b1855dca3 100644 --- a/ManiVault/res/shaders/DensityDraw.frag +++ b/ManiVault/res/shaders/DensityDraw.frag @@ -7,11 +7,11 @@ uniform sampler2D tex; uniform float norm; -in vec2 pass_texCoord; +in vec2 passUv; out vec4 fragColor; void main() { - float f = 1 - (texture(tex, pass_texCoord).r * norm); + float f = 1 - (texture(tex, passUv).r * norm); fragColor = vec4(vec3(f), 1); -} +} \ No newline at end of file diff --git a/ManiVault/res/shaders/IsoDensityDraw.frag b/ManiVault/res/shaders/IsoDensityDraw.frag index a34e3c3e2..3299a083e 100644 --- a/ManiVault/res/shaders/IsoDensityDraw.frag +++ b/ManiVault/res/shaders/IsoDensityDraw.frag @@ -10,19 +10,19 @@ uniform sampler2D densityMap; uniform vec2 renderParams; uniform vec3 colorMapRange; -in vec2 pass_texCoord; +in vec2 passUv; out vec4 fragColor; // Get the normalized density from the color map range -float getNormalizedDensity(vec2 uv) +float getNormalizedDensity(vec2 passUv) { - float density = texture(densityMap, uv).r; + float density = texture(densityMap, passUv).r; return (density - colorMapRange.x) / colorMapRange.z; } void main() { - float density = getNormalizedDensity(pass_texCoord); + float density = getNormalizedDensity(passUv); if (density < renderParams.y) discard; @@ -40,10 +40,10 @@ void main() { // Central differences to find out if we draw the iso contour instead of the color vec4 neighborDensities; - neighborDensities.x = getNormalizedDensity(pass_texCoord + texelSize.xz); - neighborDensities.y = getNormalizedDensity(pass_texCoord - texelSize.xz); - neighborDensities.z = getNormalizedDensity(pass_texCoord + texelSize.zy); - neighborDensities.w = getNormalizedDensity(pass_texCoord - texelSize.zy); + neighborDensities.x = getNormalizedDensity(passUv + texelSize.xz); + neighborDensities.y = getNormalizedDensity(passUv - texelSize.xz); + neighborDensities.z = getNormalizedDensity(passUv + texelSize.zy); + neighborDensities.w = getNormalizedDensity(passUv - texelSize.zy); ivec4 stepId = min(ivec4(floor(neighborDensities * vec4(numSteps+1))), ivec4(numSteps)); isBoundary = (any(notEqual(stepId.xxx, stepId.yzw))); diff --git a/ManiVault/res/shaders/PointPlot.vert b/ManiVault/res/shaders/PointPlot.vert index bc000a036..d77921631 100644 --- a/ManiVault/res/shaders/PointPlot.vert +++ b/ManiVault/res/shaders/PointPlot.vert @@ -12,11 +12,12 @@ #define EFFECT_COLOR_2D 4 // Point properties -uniform float pointSize; /** Point size */ -uniform float pointSizeScale; /** Scale factor in absolute point size mode */ +uniform float pointSize; /** Point size in x- and y direction to account for anisotropy of the render canvas */ +uniform bool pointSizeAbsolute; /** Whether the point size is in world or screen coordinates */ +uniform vec2 viewportSize; /** (width, height) of viewport */ uniform int scalarEffect; uniform float pointOpacity; /** Point opacity */ -uniform mat3 orthoM; /** Projection matrix from bounds space to clip space */ +uniform mat4 mvp; /** Projection matrix from bounds space to clip space */ uniform bool hasHighlights; /** Whether a highlight buffer is used */ uniform bool hasFocusHighlights; /** Whether a focus highlight buffer is used */ uniform bool hasScalars; /** Whether a scalar buffer is used */ @@ -42,6 +43,9 @@ uniform float focusOutlineOpacity; /** Focus outline opacity */ uniform bool randomizedDepthEnabled; /** Whether to randomize the z-order */ +// Miscellaneous +uniform float windowAspectRatio; /** Window aspect ratio (width / height) */ + layout(location = 0) in vec2 vertex; /** Vertex input, always a [-1, 1] quad */ layout(location = 1) in vec2 position; /** 2-Dimensional positions of points */ layout(location = 2) in int highlight; /** Mask of highlights over the points */ @@ -103,8 +107,6 @@ float floatConstruct( uint m ) { return f - 1.0; // Range [0:1] } - - // Pseudo-random value in half-open range [0:1]. float random( float x ) { return floatConstruct(hash(floatBitsToUint(x))); } float random( vec2 v ) { return floatConstruct(hash(floatBitsToUint(v))); } @@ -113,33 +115,42 @@ float random( vec4 v ) { return floatConstruct(hash(floatBitsToUint(v))); } void main() { - // The texture coordinates match vertex coordinates - vTexCoord = vertex; - - // Selection and focus highlighting - vHighlight = hasHighlights ? highlight : 0; + // Use normalized quad vertices as texture coordinates + vTexCoord = vertex; + + // Convert quad size from pixels to normalized device coordinates (NDC) + vec2 pixelSize = vec2(hasSizes ? size : pointSize) / viewportSize; + +// if (!pointSizeAbsolute) +// pixelSize /= viewportSize; + + // Apply projection only to the instance position, NOT to the quad size + vec4 worldPos = mvp * vec4(position, 0.0, 1.0); + + // Compute the scaled vertex position + vec2 scaledVertex = vertex * pixelSize; + + // Scale the vertex based on the selection display mode + scaledVertex *= ((selectionDisplayMode == 0) ? selectionOutlineScale : 1); + + // Keep quad size in screen-space while maintaining correct aspect ratio + vec2 finalPos = worldPos.xy + scaledVertex; + +// if (pointSizeAbsolute) +// finalPos = (mvp * vec4(position + scaledVertex, 0.0, 1.0)).xy; + + // Compute random depth + float depth = randomizedDepthEnabled ? random(vec2(gl_InstanceID, 0)) : 0; + + // Set the final position + gl_Position = vec4(finalPos, depth, 1.0); // Convert to NDC [-1,1] + + vHighlight = hasHighlights ? highlight : 0; vFocusHighlight = hasFocusHighlights ? focusHighlight : 0; - - vScalar = hasScalars ? (scalar - colorMapRange.x) / colorMapRange.z : 0; - - vColor = hasColors ? color : vec3(0.5); - - vOpacity = pointOpacity; + vScalar = hasScalars ? (scalar - colorMapRange.x) / colorMapRange.z : 0; + vColor = hasColors ? color : vec3(0.5); + vOpacity = pointOpacity; if (hasOpacities) vOpacity = opacity; - - vPosOrig = position; - - // Transform position to clip space - vec2 pos = (orthoM * vec3(position, 1)).xy; - - // Resize point quad according to properties - vec2 scaledVertex = vertex * pointSize * pointSizeScale * ((selectionDisplayMode == 0) ? selectionOutlineScale : 1); - - if (hasSizes) - scaledVertex = vertex * size * pointSizeScale * ((selectionDisplayMode == 0) ? selectionOutlineScale : 1); - - // Move quad by position and output - gl_Position = vec4(scaledVertex + pos, randomizedDepthEnabled ? random(pos) : 0, 1); } diff --git a/ManiVault/res/shaders/Quad.vert b/ManiVault/res/shaders/Quad.vert index 2fd2f7175..92b6da723 100644 --- a/ManiVault/res/shaders/Quad.vert +++ b/ManiVault/res/shaders/Quad.vert @@ -4,9 +4,15 @@ #version 330 core -out vec2 pass_texCoord; +layout(location = 0) in vec2 vertex; +layout(location = 1) in vec2 uv; + +uniform mat4 mvp; + +out vec2 passUv; void main() { - pass_texCoord = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); - gl_Position = vec4(pass_texCoord * 2 - 1, 0, 1); + gl_Position = mvp * vec4(vertex, 0, 1); + + passUv = uv; } diff --git a/ManiVault/src/actions/DecimalPointAction.cpp b/ManiVault/src/actions/DecimalPointAction.cpp new file mode 100644 index 000000000..f89e90b73 --- /dev/null +++ b/ManiVault/src/actions/DecimalPointAction.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#include "DecimalPointAction.h" + +using namespace mv::util; + +namespace mv::gui { + +DecimalPointAction::DecimalPointAction(QObject* parent, const QString& title) : + NumericalPointAction(parent, title, std::numeric_limits::lowest(), std::numeric_limits::max()) +{ + for (int axisIndex = 0; axisIndex < static_cast(Axis::Count); ++axisIndex) { + getAction(static_cast(axisIndex)).setNumberOfDecimals(2); + + connect(&getAction(static_cast(axisIndex)), &DecimalAction::valueChanged, this, [this](float value) -> void { + emit valueChanged(getX(), getY()); + }); + } +} + +void DecimalPointAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) +{ + auto publicDecimalPointAction = dynamic_cast(publicAction); + + Q_ASSERT(publicDecimalPointAction); + + if (!publicDecimalPointAction) + return; + + connect(this, &DecimalPointAction::valueChanged, publicDecimalPointAction, [publicDecimalPointAction](float x, float y) -> void { + publicDecimalPointAction->set(x, y); + }); + + connect(publicDecimalPointAction, &DecimalPointAction::valueChanged, this, [this](float x, float y) -> void { + set(x, y); + }); + + set(publicDecimalPointAction->get()); + + NumericalPointAction::connectToPublicAction(publicAction, recursive); +} + +void DecimalPointAction::disconnectFromPublicAction(bool recursive) +{ + if (!isConnected()) + return; + + auto publicDecimalPointAction = dynamic_cast(getPublicAction()); + + Q_ASSERT(publicDecimalPointAction); + + if (!publicDecimalPointAction) + return; + + disconnect(this, &DecimalPointAction::valueChanged, publicDecimalPointAction, nullptr); + disconnect(publicDecimalPointAction, &DecimalPointAction::valueChanged, this, nullptr); + + NumericalPointAction::disconnectFromPublicAction(recursive); +} + +} diff --git a/ManiVault/src/actions/DecimalPointAction.h b/ManiVault/src/actions/DecimalPointAction.h new file mode 100644 index 000000000..0a41ea9dd --- /dev/null +++ b/ManiVault/src/actions/DecimalPointAction.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#pragma once + +#include "NumericalPointAction.h" + +namespace mv::gui { + +/** + * Decimal action class + * + * Stores a two-dimensional decimal point value. + * + * @author Thomas Kroes + */ +class CORE_EXPORT DecimalPointAction : public NumericalPointAction +{ + Q_OBJECT + +public: + + /** + * Construct with pointer to \p parent object and \p title + * @param parent Pointer to parent object + * @param title Title of the action + */ + Q_INVOKABLE explicit DecimalPointAction(QObject* parent, const QString& title); + +protected: // Linking + + /** + * Connect this action to a public action + * @param publicAction Pointer to public action to connect to + * @param recursive Whether to also connect descendant child actions + */ + void connectToPublicAction(WidgetAction* publicAction, bool recursive) override; + + /** + * Disconnect this action from its public action + * @param recursive Whether to also disconnect descendant child actions + */ + void disconnectFromPublicAction(bool recursive) override; + +signals: + + /** + * Signals that the coordinate values changed to \p x and \p y + * @param x X coordinate value + * @param y Y coordinate value + */ + void valueChanged(float x, float y); + +protected: + static constexpr float INIT_VALUE = 0.0; /** Initialization value */ + static constexpr int INIT_DECIMALS = 1; /** Initialization number of decimals */ + + friend class AbstractActionsManager; +}; + +} + +Q_DECLARE_METATYPE(mv::gui::DecimalPointAction) + +inline const auto decimalPointActionMetaTypeId = qRegisterMetaType("mv::gui::DecimalPointAction"); diff --git a/ManiVault/src/actions/DecimalRectangleAction.cpp b/ManiVault/src/actions/DecimalRectangleAction.cpp index e6ca6f347..a64dc110c 100644 --- a/ManiVault/src/actions/DecimalRectangleAction.cpp +++ b/ManiVault/src/actions/DecimalRectangleAction.cpp @@ -21,6 +21,21 @@ DecimalRectangleAction::DecimalRectangleAction(QObject * parent, const QString& connect(&getRangeAction(Axis::Y), &DecimalRangeAction::rangeChanged, this, [this]() -> void { _rectangleChanged(); }); } +QRectF DecimalRectangleAction::toRectF() const +{ + return { + getLeft(), + getTop(), + getWidth(), + getHeight() + }; +} + +void DecimalRectangleAction::setRectF(const QRectF& rectangle) +{ + setRectangle(rectangle.left(), rectangle.right(), rectangle.bottom(), rectangle.top()); +} + void DecimalRectangleAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) { auto publicDecimalRectangleAction = dynamic_cast(publicAction); diff --git a/ManiVault/src/actions/DecimalRectangleAction.h b/ManiVault/src/actions/DecimalRectangleAction.h index a6660f1d4..87732ba2e 100644 --- a/ManiVault/src/actions/DecimalRectangleAction.h +++ b/ManiVault/src/actions/DecimalRectangleAction.h @@ -44,6 +44,18 @@ class CORE_EXPORT DecimalRectangleAction : public RectangleAction(parent, title, std::numeric_limits::lowest(), std::numeric_limits::max()) +{ + for (int axisIndex = 0; axisIndex < static_cast(Axis::Count); ++axisIndex) { + connect(&getAction(static_cast(axisIndex)), &IntegralAction::valueChanged, this, [this](std::int32_t value) -> void { + emit valueChanged(getX(), getY()); + }); + } +} + +void IntegralPointAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) +{ + auto publicIntegralPointAction = dynamic_cast(publicAction); + + Q_ASSERT(publicIntegralPointAction); + + if (!publicIntegralPointAction) + return; + + connect(this, &IntegralPointAction::valueChanged, publicIntegralPointAction, [publicIntegralPointAction](std::int32_t x, std::int32_t y) -> void { + publicIntegralPointAction->set(x, y); + }); + + connect(publicIntegralPointAction, &IntegralPointAction::valueChanged, this, [this](std::int32_t x, std::int32_t y) -> void { + set(x, y); + }); + + set(publicIntegralPointAction->get()); + + NumericalPointAction::connectToPublicAction(publicAction, recursive); +} + +void IntegralPointAction::disconnectFromPublicAction(bool recursive) +{ + if (!isConnected()) + return; + + auto publicIntegralPointAction = dynamic_cast(getPublicAction()); + + Q_ASSERT(publicIntegralPointAction); + + if (!publicIntegralPointAction) + return; + + disconnect(this, &IntegralPointAction::valueChanged, publicIntegralPointAction, nullptr); + disconnect(publicIntegralPointAction, &IntegralPointAction::valueChanged, this, nullptr); + + NumericalPointAction::disconnectFromPublicAction(recursive); +} + +} diff --git a/ManiVault/src/actions/IntegralPointAction.h b/ManiVault/src/actions/IntegralPointAction.h new file mode 100644 index 000000000..b96992eae --- /dev/null +++ b/ManiVault/src/actions/IntegralPointAction.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#pragma once + +#include "NumericalPointAction.h" + +namespace mv::gui { + +/** + * Integral action class + * + * Stores a two-dimensional integral point value. + * + * @author Thomas Kroes + */ +class CORE_EXPORT IntegralPointAction : public NumericalPointAction +{ + Q_OBJECT + +public: + + /** + * Construct with pointer to \p parent object and \p title + * @param parent Pointer to parent object + * @param title Title of the action + */ + Q_INVOKABLE explicit IntegralPointAction(QObject* parent, const QString& title); + +protected: // Linking + + /** + * Connect this action to a public action + * @param publicAction Pointer to public action to connect to + * @param recursive Whether to also connect descendant child actions + */ + void connectToPublicAction(WidgetAction* publicAction, bool recursive) override; + + /** + * Disconnect this action from its public action + * @param recursive Whether to also disconnect descendant child actions + */ + void disconnectFromPublicAction(bool recursive) override; + +signals: + + /** + * Signals that the coordinate values changed to \p x and \p y + * @param x X coordinate value + * @param y Y coordinate value + */ + void valueChanged(std::int32_t x, std::int32_t y); + + friend class AbstractActionsManager; +}; + +} + +Q_DECLARE_METATYPE(mv::gui::IntegralPointAction) + +inline const auto integralPointActionMetaTypeId = qRegisterMetaType("mv::gui::IntegralPointAction"); diff --git a/ManiVault/src/actions/NavigationAction.cpp b/ManiVault/src/actions/NavigationAction.cpp new file mode 100644 index 000000000..2d076efe4 --- /dev/null +++ b/ManiVault/src/actions/NavigationAction.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#include "NavigationAction.h" + +#include "GroupSectionTreeItem.h" + +#ifdef _DEBUG + //#define NAVIGATION_ACTION_VERBOSE +#endif + +//#define NAVIGATION_ACTION_VERBOSE + +using namespace mv::util; + +namespace mv::gui +{ + +NavigationAction::NavigationAction(QObject* parent, const QString& title) : + HorizontalGroupAction(parent, title), + _zoomOutAction(this, "Zoom out"), + _zoomPercentageAction(this, "Zoom Percentage", 0.01f, 1000.0f, 100.0f, 1), + _zoomInAction(this, "Zoom In"), + _zoomExtentsAction(this, "Zoom All"), + _zoomSelectionAction(this, "Zoom Selection"), + _zoomRegionAction(this, "Zoom Region"), + _freezeNavigation(this, "Freeze Navigation"), + _zoomRectangleAction(this, "Zoom Rectangle"), + _zoomCenterAction(this, "Zoom Center"), + _zoomFactorAction(this, "Zoom Factor") +{ + setShowLabels(false); + + _zoomOutAction.setToolTip("Zoom out by 10% (-)"); + _zoomPercentageAction.setToolTip("Zoom in/out (+)"); + _zoomInAction.setToolTip("Zoom in by 10%"); + _zoomExtentsAction.setToolTip("Zoom to the boundaries of the scene (o)"); + _zoomSelectionAction.setToolTip("Zoom to the boundaries of the current selection (b)"); + _zoomRegionAction.setToolTip("Zoom to a picked region"); + _freezeNavigation.setToolTip("Freeze the navigation"); + + _zoomPercentageAction.setOverrideSizeHint(QSize(300, 0)); + + _zoomOutAction.setIconByName("search-minus"); + _zoomInAction.setIconByName("search-plus"); + _zoomExtentsAction.setIconByName("compress"); + + _zoomSelectionAction.setIcon(StyledIcon("compress").withModifier("mouse-pointer")); + _zoomSelectionAction.setEnabled(false); + + _zoomRegionAction.setIcon(StyledIcon("compress").withModifier("search")); + + _zoomPercentageAction.setSuffix("%"); + _zoomPercentageAction.setUpdateDuringDrag(false); + + _zoomRectangleAction.setToolTip("Extents of the current view"); + _zoomRectangleAction.setIcon(combineIcons(StyledIcon("expand"), StyledIcon("ellipsis-h"))); + _zoomRectangleAction.setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + + _zoomCenterAction.setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + _zoomCenterAction.setIconByName("ruler"); + _zoomCenterAction.setDefaultWidgetFlags(GroupAction::WidgetFlag::Vertical); + _zoomCenterAction.setPopupSizeHint(QSize(250, 0)); + + _zoomCenterAction.getXAction().setDefaultWidgetFlags(DecimalAction::WidgetFlag::SpinBox); + _zoomCenterAction.getYAction().setDefaultWidgetFlags(DecimalAction::WidgetFlag::SpinBox); + + addAction(&_zoomOutAction, gui::TriggerAction::Icon); + addAction(&_zoomPercentageAction); + addAction(&_zoomInAction, gui::TriggerAction::Icon); + addAction(&_zoomCenterAction); + addAction(&_zoomExtentsAction, gui::TriggerAction::Icon); + addAction(&_zoomSelectionAction, gui::TriggerAction::Icon); + addAction(&_zoomRegionAction, gui::TriggerAction::Icon); + addAction(&_freezeNavigation, gui::ToggleAction::WidgetFlag::CheckBox); + + const auto updateReadOnly = [this]() -> void { + const auto notFrozen = _freezeNavigation.isChecked(); + + _zoomOutAction.setEnabled(!notFrozen); + _zoomPercentageAction.setEnabled(!notFrozen); + _zoomInAction.setEnabled(!notFrozen); + _zoomExtentsAction.setEnabled(!notFrozen); + _zoomSelectionAction.setEnabled(!notFrozen); + _zoomRegionAction.setEnabled(!notFrozen); + _zoomCenterAction.setEnabled(!notFrozen); + }; + + updateReadOnly(); + + connect(&_freezeNavigation, &ToggleAction::toggled, this, updateReadOnly); +} + +void NavigationAction::setShortcutsEnabled(bool shortcutsEnabled) +{ + _zoomOutAction.setShortcut(shortcutsEnabled ? QKeySequence("-") : QKeySequence()); + _zoomInAction.setShortcut(shortcutsEnabled ? QKeySequence("+") : QKeySequence()); + _zoomExtentsAction.setShortcut(shortcutsEnabled ? QKeySequence("O") : QKeySequence()); + _zoomSelectionAction.setShortcut(shortcutsEnabled ? QKeySequence("H") : QKeySequence()); + _zoomRegionAction.setShortcut(shortcutsEnabled ? QKeySequence("F") : QKeySequence()); +} + +void NavigationAction::fromVariantMap(const QVariantMap& variantMap) +{ + HorizontalGroupAction::fromVariantMap(variantMap); + + _zoomCenterAction.fromParentVariantMap(variantMap); + _zoomFactorAction.fromParentVariantMap(variantMap); +} + +QVariantMap NavigationAction::toVariantMap() const +{ + auto variantMap = HorizontalGroupAction::toVariantMap(); + + _zoomCenterAction.insertIntoVariantMap(variantMap); + _zoomFactorAction.insertIntoVariantMap(variantMap); + + return variantMap; +} + +} diff --git a/ManiVault/src/actions/NavigationAction.h b/ManiVault/src/actions/NavigationAction.h new file mode 100644 index 000000000..6e399d7fd --- /dev/null +++ b/ManiVault/src/actions/NavigationAction.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mv::gui +{ + +/** + * Navigation action class + * + * Provides actions for navigating in a renderer (the business logic + * should be handled elsewhere depending on the renderer). + * + * Note: This action is developed for internal use only + * + * @author Thomas Kroes + */ +class CORE_EXPORT NavigationAction : public HorizontalGroupAction +{ + Q_OBJECT + +public: + + /** + * Construct with pointer to \p parent object and \p title + * @param parent Pointer to parent object + * @param title Title of the action + */ + Q_INVOKABLE NavigationAction(QObject* parent, const QString& title); + + /** + * Set whether shortcuts are enabled + * @param shortcutsEnabled Boolean determining whether shortcuts are enabled + */ + void setShortcutsEnabled(bool shortcutsEnabled); + +public: // Serialization + + /** + * Load navigation action from variant map + * @param variantMap Variant map representation of the navigation action + */ + void fromVariantMap(const QVariantMap& variantMap) override; + + /** + * Save widget action to variant map + * @return Variant map representation of the widget action + */ + + QVariantMap toVariantMap() const override; + +public: // Action getters + + TriggerAction& getZoomOutAction() { return _zoomOutAction; } + DecimalAction& getZoomPercentageAction() { return _zoomPercentageAction; } + TriggerAction& getZoomInAction() { return _zoomInAction; } + TriggerAction& getZoomExtentsAction() { return _zoomExtentsAction; } + TriggerAction& getZoomSelectionAction() { return _zoomSelectionAction; } + TriggerAction& getZoomRegionAction() { return _zoomRegionAction; } + ToggleAction& getFreezeNavigation() { return _freezeNavigation; } + DecimalRectangleAction& getZoomRectangleAction() { return _zoomRectangleAction; } + DecimalPointAction& getZoomCenterAction() { return _zoomCenterAction; } + DecimalAction& getZoomFactorAction() { return _zoomFactorAction; } + +private: + TriggerAction _zoomOutAction; /** Zoom out action */ + DecimalAction _zoomPercentageAction; /** Zoom action */ + TriggerAction _zoomInAction; /** Zoom in action */ + TriggerAction _zoomExtentsAction; /** Zoom extents action */ + TriggerAction _zoomSelectionAction; /** Zoom to selection extents action */ + TriggerAction _zoomRegionAction; /** Zoom to region action */ + ToggleAction _freezeNavigation; /** Freeze navigation action */ + DecimalRectangleAction _zoomRectangleAction; /** Rectangle action for setting the current zoom bounds */ + DecimalPointAction _zoomCenterAction; /** Zoom center action */ + DecimalAction _zoomFactorAction; /** Zoom factor action */ +}; + +} diff --git a/ManiVault/src/actions/NumericalAction.h b/ManiVault/src/actions/NumericalAction.h index 126ac0c65..ad9202dfb 100644 --- a/ManiVault/src/actions/NumericalAction.h +++ b/ManiVault/src/actions/NumericalAction.h @@ -59,23 +59,15 @@ class NumericalAction : public WidgetAction _value(), _minimum(std::numeric_limits::lowest()), _maximum(std::numeric_limits::max()), - _prefix(), - _suffix(), _numberOfDecimals(), - _updateDuringDrag(true), - _valueChanged(), - _minimumChanged(), - _maximumChanged(), - _prefixChanged(), - _suffixChanged(), - _numberOfDecimalsChanged() + _updateDuringDrag(true) { setText(title); setDefaultWidgetFlags(WidgetFlag::Default); } /** Gets the current value */ - virtual NumericalType getValue() const final { + virtual NumericalType getValue() const { return _value; } @@ -101,7 +93,7 @@ class NumericalAction : public WidgetAction } /** Gets the minimum value */ - virtual NumericalType getMinimum() const final { + virtual NumericalType getMinimum() const { return _minimum; } @@ -109,7 +101,7 @@ class NumericalAction : public WidgetAction * Sets the minimum value * @param minimum Minimum value */ - virtual void setMinimum(NumericalType minimum) final { + virtual void setMinimum(NumericalType minimum) { if (minimum == _minimum) return; @@ -119,7 +111,7 @@ class NumericalAction : public WidgetAction } /** Gets the maximum value */ - virtual NumericalType getMaximum() const final { + virtual NumericalType getMaximum() const { return _maximum; } @@ -127,7 +119,7 @@ class NumericalAction : public WidgetAction * Sets the maximum value * @param maximum Maximum value */ - virtual void setMaximum(NumericalType maximum) final { + virtual void setMaximum(NumericalType maximum) { if (maximum == _maximum) return; @@ -140,7 +132,7 @@ class NumericalAction : public WidgetAction * Gets the value range * @return Range */ - virtual util::NumericalRange getRange() const final { + virtual util::NumericalRange getRange() const { return { getMinimum(), getMaximum() }; } @@ -148,7 +140,7 @@ class NumericalAction : public WidgetAction * Sets the value range * @param range Range */ - virtual void setRange(util::NumericalRange range) final { + virtual void setRange(util::NumericalRange range) { setMinimum(range.getMinimum()); setMaximum(range.getMaximum()); } @@ -158,13 +150,13 @@ class NumericalAction : public WidgetAction * @param minimum Minimum value * @param maximum Maximum value */ - virtual void setRange(NumericalType minimum, NumericalType maximum) final { + virtual void setRange(NumericalType minimum, NumericalType maximum) { setMinimum(minimum); setMaximum(maximum); } /** Gets the prefix */ - virtual QString getPrefix() const final { + virtual QString getPrefix() const { return _prefix; } @@ -172,7 +164,7 @@ class NumericalAction : public WidgetAction * Sets the prefix * @param prefix Prefix */ - virtual void setPrefix(const QString& prefix) final { + virtual void setPrefix(const QString& prefix) { if (prefix == _prefix) return; @@ -182,7 +174,7 @@ class NumericalAction : public WidgetAction } /** Gets the suffix */ - virtual QString getSuffix() const final { + virtual QString getSuffix() const { return _suffix; } @@ -190,7 +182,7 @@ class NumericalAction : public WidgetAction * Sets the suffix * @param suffix Suffix */ - virtual void setSuffix(const QString& suffix) final { + virtual void setSuffix(const QString& suffix) { if (suffix == _suffix) return; @@ -200,7 +192,7 @@ class NumericalAction : public WidgetAction } /** Gets the number of decimals */ - virtual std::uint32_t getNumberOfDecimals() const final { + virtual std::uint32_t getNumberOfDecimals() const { return _numberOfDecimals; } @@ -208,7 +200,7 @@ class NumericalAction : public WidgetAction * Sets the number of decimals * @param numberOfDecimals number of decimals */ - virtual void setNumberOfDecimals(std::uint32_t numberOfDecimals) final { + virtual void setNumberOfDecimals(std::uint32_t numberOfDecimals) { if (numberOfDecimals == _numberOfDecimals) return; @@ -218,7 +210,7 @@ class NumericalAction : public WidgetAction } /** Gets whether the value should update during interaction */ - virtual bool getUpdateDuringDrag() const final { + virtual bool getUpdateDuringDrag() const { return _updateDuringDrag; } @@ -226,7 +218,7 @@ class NumericalAction : public WidgetAction * Sets whether the value should update during interaction * @param updateDuringDrag Whether the value should update during interaction */ - virtual void setUpdateDuringDrag(bool updateDuringDrag) final { + virtual void setUpdateDuringDrag(bool updateDuringDrag) { if (updateDuringDrag == _updateDuringDrag) return; @@ -234,23 +226,24 @@ class NumericalAction : public WidgetAction } /** Returns whether the current value is at its minimum */ - virtual bool isAtMinimum() const final { + virtual bool isAtMinimum() const { return _value == _minimum; } /** Returns whether the current value is at its maximum */ - virtual bool isAtMaximum() const final { + virtual bool isAtMaximum() const { return _value == _maximum; } /** Returns the length of the interval defined by the minimum and maximum value */ - virtual double getIntervalLength() const final { + virtual double getIntervalLength() const { return static_cast(_maximum) - static_cast(_minimum); } /** Returns the normalized value */ - virtual double getNormalized() const final { + virtual double getNormalized() const { const auto offset = static_cast(_value) - static_cast(_minimum); + return static_cast(offset / getIntervalLength()); } diff --git a/ManiVault/src/actions/NumericalPointAction.cpp b/ManiVault/src/actions/NumericalPointAction.cpp new file mode 100644 index 000000000..d3bca223e --- /dev/null +++ b/ManiVault/src/actions/NumericalPointAction.cpp @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#include "NumericalPointAction.h" + +namespace mv::gui { + +} \ No newline at end of file diff --git a/ManiVault/src/actions/NumericalPointAction.h b/ManiVault/src/actions/NumericalPointAction.h new file mode 100644 index 000000000..4626af3b6 --- /dev/null +++ b/ManiVault/src/actions/NumericalPointAction.h @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#pragma once + +#include "GroupAction.h" + +#include +#include + +namespace mv::gui { + +/** + * Numerical point action base class + * + * Stores a two-dimensional point value. + * + * @author Thomas Kroes + */ +template +class NumericalPointAction : public GroupAction +{ +public: + + /** Axis enum */ + enum class Axis { + X = 0, /** X axis */ + Y, /** Y axis */ + + Count + }; + +public: + + /** + * Construct with pointer to \p parent object and \p title + * @param parent Pointer to parent object + * @param title Title of the action + * @param minimum Minimum value + * @param maximum Maximum value + */ + NumericalPointAction(QObject* parent, const QString& title, NumericalType minimum, NumericalType maximum) : + GroupAction(parent, title), + _elementActions{ + NumericalActionType(this, "X", minimum, maximum), + NumericalActionType(this, "Y", minimum, maximum) + } + { + setText(title); + setDefaultWidgetFlags(WidgetFlag::Default); + + addAction(&getXAction()); + addAction(&getYAction()); + } + + /** + * Retrieves the X coordinate value + * @return The X coordinate + */ + NumericalType getX() const { return getXAction().getValue(); } + + /** + * Set the X coordinate value + * @param x X coordinate value + */ + void setX(const NumericalType& x) { + getXAction().setValue(x); + } + + /** + * Set the Y coordinate value + * @param y Y coordinate value + */ + void setY(const NumericalType& y) { + getYAction().setValue(y); + } + + /** + * Retrieves the Y coordinate value + * @return The Y coordinate + */ + NumericalType getY() const { return getYAction().getValue(); } + + /** + * Set the X and Y coordinate values + * @param x X coordinate value + * @param y Y coordinate value + */ + void set(const NumericalType& x, const NumericalType& y) { + setX(x); + setY(y); + } + + /** + * Retrieves the X and Y coordinate values as a QPoint + * @return QPoint value + */ + QPoint getPoint() const { + return QPoint(static_cast(getX()), static_cast(getY())); + } + + /** + * Set the X and Y coordinate values from a QPoint + * @param point QPoint value + */ + void set(const QPoint& point) { + set(static_cast(point.x()), static_cast(point.y())); + } + + /** + * Retrieves the X and Y coordinate values as a QPointF + * @return QPointF value + */ + QPointF getPointF() const { + return QPointF(getX(), getY()); + } + + /** + * Set the X and Y coordinate values from a QPointF + * @param point QPointF value + */ + void set(const QPointF& point) { + set(static_cast(point.x()), static_cast(point.y())); + } + + /** + * Retrieves the X and Y coordinate values as a std::pair + * @return Pair of X and Y coordinate values + */ + std::pair get() const { + return std::make_pair(getX(), getY()); + } + + /** + * Set the X and Y coordinate values from a std::pair + * @param pair Pair of X and Y coordinate values + */ + void set(const std::pair& pair) { + set(pair.first, pair.second); + } + + /** + * Retrieves the X and Y coordinate values as a QVector2D + * @return QVector2D value + */ + QVector2D getVector() const { + return QVector2D(getX(), getY()); + } + + /** + * Set the X and Y coordinate values from a Qt vector + * @param vector QVector2D value + */ + void set(const QVector2D& vector) { + set(static_cast(vector.x()), static_cast(vector.y())); + } + +public: // Serialization + + /** + * Load numerical point action from variant map + * @param variantMap Variant map representation of the numerical point action + */ + void fromVariantMap(const QVariantMap& variantMap) override + { + WidgetAction::fromVariantMap(variantMap); + + for (int axisIndex = 0; axisIndex < static_cast(Axis::Count); ++axisIndex) + _elementActions[axisIndex].fromParentVariantMap(variantMap); + } + + /** + * Save numerical point action to variant map + * @return Variant map representation of the numerical point action + */ + QVariantMap toVariantMap() const override + { + auto variantMap = WidgetAction::toVariantMap(); + + for (int axisIndex = 0; axisIndex < static_cast(Axis::Count); ++axisIndex) + _elementActions[axisIndex].insertIntoVariantMap(variantMap); + + return variantMap; + } + +public: // Action getters + + const NumericalActionType& getAction(const Axis& axis) const { return _elementActions[static_cast(axis)]; } + const NumericalActionType& getXAction() const { return getAction(Axis::X); } + const NumericalActionType& getYAction() const { return getAction(Axis::Y); } + + NumericalActionType& getAction(const Axis& axis) { return _elementActions[static_cast(axis)]; } + NumericalActionType& getXAction() { return getAction(Axis::X); } + NumericalActionType& getYAction() { return getAction(Axis::Y); } + +private: + NumericalActionType _elementActions[static_cast(Axis::Count)]; /** Elements actions */ +}; + +} diff --git a/ManiVault/src/actions/PixelSelectionAction.cpp b/ManiVault/src/actions/PixelSelectionAction.cpp index be774cff0..024cc8e73 100644 --- a/ManiVault/src/actions/PixelSelectionAction.cpp +++ b/ManiVault/src/actions/PixelSelectionAction.cpp @@ -61,32 +61,26 @@ PixelSelectionAction::PixelSelectionAction(QObject* parent, const QString& title _rectangleAction.setIcon(getPixelSelectionTypeIcon(PixelSelectionType::Rectangle)); _rectangleAction.setToolTip("Select pixels inside a rectangle (R)"); _rectangleAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _rectangleAction.setShortcut(QKeySequence("R")); _brushAction.setIcon(getPixelSelectionTypeIcon(PixelSelectionType::Brush)); _brushAction.setToolTip("Select pixels using a brush tool (B)"); _brushAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _brushAction.setShortcut(QKeySequence("B")); _lassoAction.setIcon(getPixelSelectionTypeIcon(PixelSelectionType::Lasso)); _lassoAction.setToolTip("Select pixels using a lasso (L)"); _lassoAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _lassoAction.setShortcut(QKeySequence("L")); _polygonAction.setIcon(getPixelSelectionTypeIcon(PixelSelectionType::Polygon)); _polygonAction.setToolTip("Select pixels by drawing a polygon (P)"); _polygonAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _polygonAction.setShortcut(QKeySequence("P")); _sampleAction.setIcon(getPixelSelectionTypeIcon(PixelSelectionType::Sample)); _sampleAction.setToolTip("Sample pixel by dragging over the image (S)"); _sampleAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _sampleAction.setShortcut(QKeySequence("S")); _roiAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); _roiAction.setIcon(getPixelSelectionTypeIcon(PixelSelectionType::ROI)); _roiAction.setToolTip("Sample within region of interest (I)"); - _roiAction.setShortcut(QKeySequence("I")); _modifierAction.setToolTip("Type of selection modifier"); _modifierAction.setCurrentIndex(static_cast(PixelSelectionModifierType::Replace)); @@ -113,22 +107,20 @@ PixelSelectionAction::PixelSelectionAction(QObject* parent, const QString& title _clearSelectionAction.setToolTip("Clears the selection (E)"); _clearSelectionAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _clearSelectionAction.setShortcut(QKeySequence("E")); - _selectAllAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _selectAllAction.setShortcut(QKeySequence("A")); _selectAllAction.setToolTip("Select all data points (A)"); - + _invertSelectionAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _invertSelectionAction.setShortcut(QKeySequence("I")); _invertSelectionAction.setToolTip("Invert the selection (I)"); + _invertSelectionAction.setToolTip("Invert the selection (I)"); + _notifyDuringSelectionAction.setShortcut(QKeySequence("U")); + _brushRadiusAction.setToolTip("Brush selection tool radius"); _brushRadiusAction.setSuffix("px"); _notifyDuringSelectionAction.setDefaultWidgetFlags(ToggleAction::CheckBox); _notifyDuringSelectionAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _notifyDuringSelectionAction.setShortcut(QKeySequence("U")); _notifyDuringSelectionAction.setToolTip("Notify during selection or only at the end of the selection process (U)"); const auto updatePixelSelectionTypesModel = [this]() { @@ -207,6 +199,16 @@ void PixelSelectionAction::initialize(QWidget* targetWidget, util::PixelSelectio setShortcutsEnabled(true); + _targetWidget->addAction(&_rectangleAction); + _targetWidget->addAction(&_brushAction); + _targetWidget->addAction(&_lassoAction); + _targetWidget->addAction(&_polygonAction); + _targetWidget->addAction(&_sampleAction); + _targetWidget->addAction(&_roiAction); + _targetWidget->addAction(&_selectAllAction); + _targetWidget->addAction(&_clearSelectionAction); + _targetWidget->addAction(&_invertSelectionAction); + _targetWidget->installEventFilter(this); _initialized = true; } @@ -226,34 +228,15 @@ void PixelSelectionAction::setShortcutsEnabled(const bool& shortcutsEnabled) if (!isInitialized()) return; - if (shortcutsEnabled) { - _targetWidget->addAction(&_rectangleAction); - _targetWidget->addAction(&_brushAction); - _targetWidget->addAction(&_lassoAction); - _targetWidget->addAction(&_polygonAction); - _targetWidget->addAction(&_sampleAction); - _targetWidget->addAction(&_modifierAddAction); - _targetWidget->addAction(&_modifierSubtractAction); - _targetWidget->addAction(&_clearSelectionAction); - _targetWidget->addAction(&_selectAllAction); - _targetWidget->addAction(&_invertSelectionAction); - _targetWidget->addAction(&_brushRadiusAction); - _targetWidget->addAction(&_notifyDuringSelectionAction); - } - else { - _targetWidget->removeAction(&_rectangleAction); - _targetWidget->removeAction(&_brushAction); - _targetWidget->removeAction(&_lassoAction); - _targetWidget->removeAction(&_polygonAction); - _targetWidget->removeAction(&_sampleAction); - _targetWidget->removeAction(&_modifierAddAction); - _targetWidget->removeAction(&_modifierSubtractAction); - _targetWidget->removeAction(&_clearSelectionAction); - _targetWidget->removeAction(&_selectAllAction); - _targetWidget->removeAction(&_invertSelectionAction); - _targetWidget->removeAction(&_brushRadiusAction); - _targetWidget->removeAction(&_notifyDuringSelectionAction); - } + _rectangleAction.setShortcut(shortcutsEnabled ? QKeySequence("R") : QKeySequence()); + _brushAction.setShortcut(shortcutsEnabled ? QKeySequence("B") : QKeySequence()); + _lassoAction.setShortcut(shortcutsEnabled ? QKeySequence("L") : QKeySequence()); + _polygonAction.setShortcut(shortcutsEnabled ? QKeySequence("P") : QKeySequence()); + _sampleAction.setShortcut(shortcutsEnabled ? QKeySequence("U") : QKeySequence()); + _roiAction.setShortcut(shortcutsEnabled ? QKeySequence("W") : QKeySequence()); + _selectAllAction.setShortcut(shortcutsEnabled ? QKeySequence("A") : QKeySequence()); + _clearSelectionAction.setShortcut(shortcutsEnabled ? QKeySequence("E") : QKeySequence()); + _invertSelectionAction.setShortcut(shortcutsEnabled ? QKeySequence("I") : QKeySequence()); } void PixelSelectionAction::initType() @@ -417,15 +400,15 @@ QMenu* PixelSelectionAction::getContextMenu() bool PixelSelectionAction::eventFilter(QObject* object, QEvent* event) { if (!isEnabled()) - return QObject::eventFilter(object, event); + return QWidgetAction::eventFilter(object, event); const auto keyEvent = dynamic_cast(event); if (!keyEvent) - return QObject::eventFilter(object, event); + return QWidgetAction::eventFilter(object, event); if (keyEvent->isAutoRepeat()) - return QObject::eventFilter(object, event); + return QWidgetAction::eventFilter(object, event); switch (keyEvent->type()) { @@ -471,7 +454,7 @@ bool PixelSelectionAction::eventFilter(QObject* object, QEvent* event) break; } - return QObject::eventFilter(object, event); + return QWidgetAction::eventFilter(object, event); } void PixelSelectionAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) diff --git a/ManiVault/src/actions/ViewPluginSamplerAction.cpp b/ManiVault/src/actions/ViewPluginSamplerAction.cpp index 983e06c92..606c84e84 100644 --- a/ManiVault/src/actions/ViewPluginSamplerAction.cpp +++ b/ManiVault/src/actions/ViewPluginSamplerAction.cpp @@ -442,20 +442,26 @@ bool ViewPluginSamplerAction::eventFilter(QObject* target, QEvent* event) case QEvent::MouseButtonPress: { - _samplerPixelSelectionAction->getPixelSelectionTool()->setEnabled(false); + _samplerPixelSelectionAction->getPixelSelectionTool()->setEnabled(false); + _toolTipLabel.hide(); break; } case QEvent::MouseButtonRelease: case QEvent::Enter: { - _samplerPixelSelectionAction->getPixelSelectionTool()->setEnabled(getSamplingMode() == SamplingMode::FocusRegion && getEnabledAction().isChecked() && canView()); + const auto enabled = getSamplingMode() == SamplingMode::FocusRegion && getEnabledAction().isChecked() && canView(); + + _samplerPixelSelectionAction->getPixelSelectionTool()->setEnabled(enabled); + _toolTipLabel.setVisible(enabled); + break; } case QEvent::Leave: { _samplerPixelSelectionAction->getPixelSelectionTool()->setEnabled(false); + _toolTipLabel.hide(); break; } diff --git a/ManiVault/src/actions/WidgetAction.h b/ManiVault/src/actions/WidgetAction.h index 979dbcfdf..6210ed45c 100644 --- a/ManiVault/src/actions/WidgetAction.h +++ b/ManiVault/src/actions/WidgetAction.h @@ -772,14 +772,12 @@ class CORE_EXPORT WidgetAction : public QWidgetAction, public util::Serializable * Get override size hint * @return Override size hint */ - [[deprecated("This method is a placeholder and not operational yet")]] QSize getOverrideSizeHint() const; /** * Set override size hint * @param overrideSizeHint Override size hint */ - [[deprecated("This method is a placeholder and not operational yet")]] void setOverrideSizeHint(const QSize& overrideSizeHint); public: // Configuration flags diff --git a/ManiVault/src/actions/WidgetActionWidget.cpp b/ManiVault/src/actions/WidgetActionWidget.cpp index 27945d924..a5732c123 100644 --- a/ManiVault/src/actions/WidgetActionWidget.cpp +++ b/ManiVault/src/actions/WidgetActionWidget.cpp @@ -30,8 +30,8 @@ QSize WidgetActionWidget::sizeHint() const return popupSizeHint; } - //if (!action->getOverrideSizeHint().isNull()) - // return action->getOverrideSizeHint(); + if (action->getOverrideSizeHint().width() > 0 && action->getOverrideSizeHint().height() > 0) + return action->getOverrideSizeHint(); return QWidget::sizeHint(); } diff --git a/ManiVault/src/graphics/Shader.cpp b/ManiVault/src/graphics/Shader.cpp index dd99c67b6..7be631a37 100644 --- a/ManiVault/src/graphics/Shader.cpp +++ b/ManiVault/src/graphics/Shader.cpp @@ -156,9 +156,13 @@ void ShaderProgram::uniformMatrix3f(const char* name, Matrix3f& m) { glUniformMatrix3fv(location(name), 1, false, m.toArray()); } -//void Shader::uniformMatrix4f(const char* name, Matrix4f& m) { -// glUniformMatrix4fv(location(name), 1, false, m.toArray()); -//} +void ShaderProgram::uniformMatrix3f(const char* name, float* data) { + glUniformMatrix3fv(location(name), 1, false, data); +} + +void ShaderProgram::uniformMatrix4f(const char* name, float* data) { + glUniformMatrix4fv(location(name), 1, false, data); +} void ShaderProgram::uniformMatrix4f(const char* name, const float* const m) { diff --git a/ManiVault/src/graphics/Shader.h b/ManiVault/src/graphics/Shader.h index 2ee0c49ce..b8fb2648f 100644 --- a/ManiVault/src/graphics/Shader.h +++ b/ManiVault/src/graphics/Shader.h @@ -36,6 +36,11 @@ class CORE_EXPORT ShaderProgram : protected QOpenGLFunctions_3_3_Core void uniform3fv(const char* name, int count, Vector3f* v); void uniform4f(const char* name, float v0, float v1, float v2, float v3); void uniformMatrix3f(const char* name, Matrix3f& m); + + void uniformMatrix3f(const char* name, float* data); + + void uniformMatrix4f(const char* name, float* data); + //void uniformMatrix4f(const char* name, Matrix4f& m); void uniformMatrix4f(const char* name, const float* const m); diff --git a/ManiVault/src/renderers/DensityRenderer.cpp b/ManiVault/src/renderers/DensityRenderer.cpp index c7237ed8f..c0189817d 100644 --- a/ManiVault/src/renderers/DensityRenderer.cpp +++ b/ManiVault/src/renderers/DensityRenderer.cpp @@ -4,174 +4,268 @@ #include "DensityRenderer.h" -namespace mv +#ifdef _DEBUG + //#define DENSITY_RENDERER_VERBOSE +#endif + +namespace mv::gui { - namespace gui - { +DensityRenderer::DensityRenderer(RenderMode renderMode, QWidget* sourceWidget, QObject* parent) : + Renderer2D(parent), + _renderMode(renderMode) +{ + if (sourceWidget) + setSourceWidget(sourceWidget); +} - DensityRenderer::DensityRenderer(RenderMode renderMode) - : - _renderMode(renderMode) - { +DensityRenderer::~DensityRenderer() +{ + // Delete objects + _densityComputation.cleanup(); +} - } +void DensityRenderer::resize(QSize renderSize) +{ + Renderer2D::resize(renderSize); - DensityRenderer::~DensityRenderer() - { - // Delete objects - _densityComputation.cleanup(); - } + updateQuad(); +} - void DensityRenderer::setRenderMode(RenderMode renderMode) - { - _renderMode = renderMode; - } +void DensityRenderer::setDataBounds(const QRectF& dataBounds) +{ + Renderer2D::setDataBounds(dataBounds); - // Points need to be passed as a pointer as we need to store them locally in order - // to be able to recompute the densities when parameters change. - void DensityRenderer::setData(const std::vector* points) - { - _densityComputation.setData(points); - } + updateQuad(); +} - void DensityRenderer::setWeights(const std::vector* weights) - { - _densityComputation.setWeights(weights); - } +QRectF DensityRenderer::computeWorldBounds() const +{ + const auto squareSize = std::max(getDataBounds().width(), getDataBounds().height()); + const auto squareDataBounds = QRectF(getDataBounds().center() - QPointF(squareSize / 2.f, squareSize / 2.f), QSizeF(squareSize, squareSize)); + const auto marginX = getNavigator().getZoomMarginScreen() * static_cast(getDataBounds().width()) / (static_cast(getRenderSize().height() - 2.f * getNavigator().getZoomMarginScreen())); + const auto marginY = getNavigator().getZoomMarginScreen() * static_cast(getDataBounds().height()) / (static_cast(getRenderSize().width() - 2.f * getNavigator().getZoomMarginScreen())); + const auto margin = std::max(marginX, marginY); + const auto margins = QMarginsF(margin, margin, margin, margin); - void DensityRenderer::setBounds(const Bounds& bounds) - { - _densityComputation.setBounds(bounds.getLeft(), bounds.getRight(), bounds.getBottom(), bounds.getTop()); - } + return squareDataBounds.marginsAdded(margins); +} - void DensityRenderer::setSigma(const float sigma) - { - _densityComputation.setSigma(sigma); - } +void DensityRenderer::setDensityComputationDataBounds(const QRectF& bounds) +{ + _densityComputation.setBounds(bounds.left(), bounds.right(), bounds.bottom(), bounds.top()); +} - void DensityRenderer::computeDensity() - { - _densityComputation.compute(); - } +void DensityRenderer::setRenderMode(RenderMode renderMode) +{ + _renderMode = renderMode; +} - float DensityRenderer::getMaxDensity() const - { - return _densityComputation.getMaxDensity(); - } +// Points need to be passed as a pointer as we need to store them locally in order +// to be able to recompute the densities when parameters change. +void DensityRenderer::setData(const std::vector* points) +{ + _densityComputation.setData(points); +} - mv::Vector3f DensityRenderer::getColorMapRange() const - { - return Vector3f(0.0f, _densityComputation.getMaxDensity(), _densityComputation.getMaxDensity()); - } +void DensityRenderer::setWeights(const std::vector* weights) +{ + _densityComputation.setWeights(weights); +} - void DensityRenderer::setColormap(const QImage& image) - { - _colormap.loadFromImage(image); - _hasColorMap = true; - } +void DensityRenderer::setSigma(const float sigma) +{ + _densityComputation.setSigma(sigma); +} - void DensityRenderer::init() - { - initializeOpenGLFunctions(); +void DensityRenderer::computeDensity() +{ + _densityComputation.compute(); +} - // Create a simple VAO for full-screen quad rendering - glGenVertexArrays(1, &_quad); +float DensityRenderer::getMaxDensity() const +{ + return _densityComputation.getMaxDensity(); +} - // Load the necessary shaders for density drawing - bool loaded = true; - loaded &= _shaderDensityDraw.loadShaderFromFile(":shaders/Quad.vert", ":shaders/DensityDraw.frag"); - loaded &= _shaderIsoDensityDraw.loadShaderFromFile(":shaders/Quad.vert", ":shaders/IsoDensityDraw.frag"); - if (!loaded) { - qDebug() << "Failed to load one of the Density shaders"; - } +mv::Vector3f DensityRenderer::getColorMapRange() const +{ + return { + 0.0f, + _densityComputation.getMaxDensity(), + _densityComputation.getMaxDensity() + }; +} + +void DensityRenderer::setColormap(const QImage& image) +{ + _colormap.loadFromImage(image); + _hasColorMap = true; +} - // Initialize the density computation - _densityComputation.init(QOpenGLContext::currentContext()); - } +void DensityRenderer::init() +{ + initializeOpenGLFunctions(); - void DensityRenderer::resize(QSize renderSize) - { - int w = renderSize.width(); - int h = renderSize.height(); + updateQuad(); - _windowSize.setWidth(w); - _windowSize.setHeight(h); - } + // Load the necessary shaders for density drawing + bool loaded = true; + loaded &= _shaderDensityDraw.loadShaderFromFile(":shaders/Quad.vert", ":shaders/DensityDraw.frag"); + loaded &= _shaderIsoDensityDraw.loadShaderFromFile(":shaders/Quad.vert", ":shaders/IsoDensityDraw.frag"); - void DensityRenderer::render() - { - glViewport(0, 0, _windowSize.width(), _windowSize.height()); + if (!loaded) { + qDebug() << "Failed to load one of the Density shaders"; + } - int w = _windowSize.width(); - int h = _windowSize.height(); - int size = w < h ? w : h; - glViewport(w / 2 - size / 2, h / 2 - size / 2, size, size); + // Initialize the density computation + _densityComputation.init(QOpenGLContext::currentContext()); +} - // Draw density or isolines map - switch (_renderMode) { - case DENSITY: drawDensity(); break; - case LANDSCAPE: drawLandscape(); break; - } +void DensityRenderer::render() +{ + beginRender(); + { + switch (_renderMode) { + case DENSITY: { + drawDensity(); + break; + } + + case LANDSCAPE: { + drawLandscape(); + break; + } } + } + endRender(); +} - void DensityRenderer::destroy() - { - _shaderDensityDraw.destroy(); - _shaderIsoDensityDraw.destroy(); - _densityComputation.cleanup(); - _colormap.destroy(); +void DensityRenderer::destroy() +{ + _shaderDensityDraw.destroy(); + _shaderIsoDensityDraw.destroy(); + _densityComputation.cleanup(); + _colormap.destroy(); - glDeleteVertexArrays(1, &_quad); - } + glDeleteVertexArrays(1, &_VAO); + glDeleteVertexArrays(1, &_VBO); + glDeleteVertexArrays(1, &_EBO); +} - void DensityRenderer::drawFullscreenQuad() - { - glBindVertexArray(_quad); - glDrawArrays(GL_TRIANGLES, 0, 3); - glBindVertexArray(0); - } +void DensityRenderer::updateQuad() +{ + const auto worldBounds = getDataBounds();//computeWorldBounds(); + const auto left = static_cast(worldBounds.left()); + const auto right = static_cast(worldBounds.right()); + const auto top = static_cast(worldBounds.top()); + const auto bottom = static_cast(worldBounds.bottom()); - void DensityRenderer::drawDensity() - { - float maxDensity = _densityComputation.getMaxDensity(); - if (maxDensity <= 0) { return; } + float vertices[] = { + left, bottom, 0.0f, 0.0f, + right, bottom, 1.0f, 0.0f, + right, top, 1.0f, 1.0f, + left, top, 0.0f, 1.0f + }; - _shaderDensityDraw.bind(); + unsigned int indices[] = { + 0, 1, 2, + 2, 3, 0 + }; - _densityComputation.getDensityTexture().bind(0); - _shaderDensityDraw.uniform1i("tex", 0); - _shaderDensityDraw.uniform1f("norm", 1 / maxDensity); + glGenVertexArrays(1, &_VAO); + glGenBuffers(1, &_VBO); + glGenBuffers(1, &_EBO); - drawFullscreenQuad(); - } + glBindVertexArray(_VAO); - void DensityRenderer::drawLandscape() - { - if (!_hasColorMap) - return; + glBindBuffer(GL_ARRAY_BUFFER, _VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - float maxDensity = _densityComputation.getMaxDensity(); - if (maxDensity <= 0) { return; } + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - _shaderIsoDensityDraw.bind(); + // Position attribute + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); - _densityComputation.getDensityTexture().bind(0); - _shaderIsoDensityDraw.uniform1i("tex", 0); + // Texture coordinates attribute + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); + glEnableVertexAttribArray(1); - _shaderIsoDensityDraw.uniform2f("renderParams", 1.0f / maxDensity, 1.0f / _densityComputation.getNumPoints()); - _shaderIsoDensityDraw.uniform3f("colorMapRange", _colorMapRange); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +} - _colormap.bind(1); - _shaderIsoDensityDraw.uniform1i("colormap", 1); +void DensityRenderer::drawQuad() +{ + updateQuad(); - drawFullscreenQuad(); - } + glBindVertexArray(_VAO); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); +} - void DensityRenderer::setColorMapRange(const float& min, const float& max) - { - _colorMapRange = Vector3f(min, max, max - min); - } +void DensityRenderer::drawDensity() +{ +#ifdef DENSITY_RENDERER_VERBOSE + qDebug() << __FUNCTION__; +#endif + + const auto maxDensity = _densityComputation.getMaxDensity(); + + if (maxDensity <= 0) { + return; + } + + _shaderDensityDraw.bind(); + { + _densityComputation.getDensityTexture().bind(0); - } // namespace gui + _shaderDensityDraw.uniformMatrix4f("mvp", getModelViewProjectionMatrix().data()); + _shaderDensityDraw.uniform1i("tex", 0); + _shaderDensityDraw.uniform1f("norm", 1.0f / maxDensity); + + drawQuad(); + } + _shaderDensityDraw.release(); +} + +void DensityRenderer::drawLandscape() +{ + if (!_hasColorMap) + return; + +#ifdef DENSITY_RENDERER_VERBOSE + qDebug() << __FUNCTION__; +#endif + + const auto maxDensity = _densityComputation.getMaxDensity(); + + if (maxDensity <= 0) { + return; + } + + _shaderIsoDensityDraw.bind(); + { + _densityComputation.getDensityTexture().bind(0); + + _shaderIsoDensityDraw.uniformMatrix4f("mvp", getModelViewProjectionMatrix().data()); + _shaderIsoDensityDraw.uniform1i("tex", 0); + _shaderIsoDensityDraw.uniform2f("renderParams", 1.0f / maxDensity, 1.0f / _densityComputation.getNumPoints()); + _shaderIsoDensityDraw.uniform3f("colorMapRange", _colorMapRange); + + _colormap.bind(1); + + _shaderIsoDensityDraw.uniform1i("colormap", 1); + + drawQuad(); + } + _shaderIsoDensityDraw.release(); +} + +void DensityRenderer::setColorMapRange(const float& min, const float& max) +{ + _colorMapRange = Vector3f(min, max, max - min); +} -} // namespace mv +} diff --git a/ManiVault/src/renderers/DensityRenderer.h b/ManiVault/src/renderers/DensityRenderer.h index 8f5826940..47afef1b5 100644 --- a/ManiVault/src/renderers/DensityRenderer.h +++ b/ManiVault/src/renderers/DensityRenderer.h @@ -4,9 +4,8 @@ #pragma once -#include "Renderer.h" +#include "Renderer2D.h" -#include "graphics/Bounds.h" #include "graphics/Shader.h" #include "graphics/Texture.h" #include "graphics/Vector2f.h" @@ -14,71 +13,87 @@ #include "util/MeanShift.h" -#include +namespace mv::gui +{ -namespace mv +class CORE_EXPORT DensityRenderer : public Renderer2D { - namespace gui - { - class CORE_EXPORT DensityRenderer : public Renderer - { +public: + enum RenderMode { + DENSITY, LANDSCAPE + }; + + DensityRenderer(RenderMode renderMode, QWidget* sourceWidget = nullptr, QObject* parent = nullptr); + ~DensityRenderer() override; + + /** + * Resize the renderer to \p renderSize + * @param renderSize New size of the renderer + */ + void resize(QSize renderSize) override; + + /** + * Set data bounds to \p dataBounds + * @param dataBounds Data bounds + */ + void setDataBounds(const QRectF& dataBounds) override; - public: - enum RenderMode { - DENSITY, LANDSCAPE - }; + /** Update the world bounds */ + QRectF computeWorldBounds() const override; - DensityRenderer(RenderMode renderMode); - ~DensityRenderer() override; + /** + * Set density computation data boundss + * @param bounds Density computation data bounds + */ + void setDensityComputationDataBounds(const QRectF& bounds); - void setRenderMode(RenderMode renderMode); - void setData(const std::vector* data); - void setWeights(const std::vector* weights); - void setBounds(const Bounds& bounds); - void setSigma(const float sigma); - void computeDensity(); - float getMaxDensity() const; - Vector3f getColorMapRange() const; + void setRenderMode(RenderMode renderMode); + void setData(const std::vector* data); + void setWeights(const std::vector* weights); + void setSigma(const float sigma); + void computeDensity(); + float getMaxDensity() const; + Vector3f getColorMapRange() const; - /** - * Loads a colormap from an image and loads as - *the current colormap for the landscape view. - * @param image Color map image - */ - void setColormap(const QImage& image); + /** + * Loads a colormap from an image and loads as + *the current colormap for the landscape view. + * @param image Color map image + */ + void setColormap(const QImage& image); - void init() override; - void resize(QSize renderSize) override; - void render() override; - void destroy() override; + void init() override; - void setColorMapRange(const float& min, const float& max); + void render() override; - private: - void drawDensity(); - void drawLandscape(); + void destroy() override; - void drawFullscreenQuad(); + void setColorMapRange(const float& min, const float& max); - private: - QSize _windowSize; +private: + void drawDensity(); + void drawLandscape(); - bool _isSelecting = false; - bool _hasColorMap = false; + void updateQuad(); + void drawQuad(); - ShaderProgram _shaderDensityDraw; - ShaderProgram _shaderIsoDensityDraw; - DensityComputation _densityComputation; - Texture2D _colormap; - - Vector3f _colorMapRange; +private: + bool _isSelecting = false; + bool _hasColorMap = false; - RenderMode _renderMode; + ShaderProgram _shaderDensityDraw; + ShaderProgram _shaderIsoDensityDraw; + DensityComputation _densityComputation; + Texture2D _colormap; + + Vector3f _colorMapRange; - GLuint _quad = 0; - }; + RenderMode _renderMode; - } // namespace gui + GLuint _VAO = 0; + GLuint _VBO = 0; + GLuint _EBO = 0; +}; -} // namespace mv +} diff --git a/ManiVault/src/renderers/ImageRenderer.cpp b/ManiVault/src/renderers/ImageRenderer.cpp deleted file mode 100644 index a6dc29b9c..000000000 --- a/ManiVault/src/renderers/ImageRenderer.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// A corresponding LICENSE file is located in the root directory of this source tree -// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) - -#include "ImageRenderer.h" - -namespace mv -{ - void ImageRenderer::init() - { - - } - - void ImageRenderer::resize(QSize renderSize) - { - - } - - void ImageRenderer::render() - { - - } - - void ImageRenderer::destroy() - { - - } -} diff --git a/ManiVault/src/renderers/ImageRenderer.h b/ManiVault/src/renderers/ImageRenderer.h deleted file mode 100644 index 319dcb808..000000000 --- a/ManiVault/src/renderers/ImageRenderer.h +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// A corresponding LICENSE file is located in the root directory of this source tree -// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) - -#pragma once - -#include "Renderer.h" - -namespace mv -{ - class CORE_EXPORT ImageRenderer : public Renderer - { - public: - void init() override; - void resize(QSize renderSize) override; - void render() override; - void destroy() override; - private: - ShaderProgram _shader; - }; -} // namespace mv diff --git a/ManiVault/src/renderers/Navigator2D.cpp b/ManiVault/src/renderers/Navigator2D.cpp new file mode 100644 index 000000000..badc657c4 --- /dev/null +++ b/ManiVault/src/renderers/Navigator2D.cpp @@ -0,0 +1,761 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#include "Navigator2D.h" + +#include "Renderer2D.h" + +#include + +#ifdef _DEBUG + //#define NAVIGATOR_2D_VERBOSE +#endif + +//#define NAVIGATOR_2D_VERBOSE + +using namespace mv::gui; + +namespace mv +{ + +Navigator2D::ZoomOverlayWidget::ZoomOverlayWidget(Navigator2D& navigator, QWidget* targetWidget): + gui::OverlayWidget(targetWidget), + _navigator(navigator) +{ +} + +void Navigator2D::ZoomOverlayWidget::paintEvent(QPaintEvent* event) +{ + if (_navigator.getZoomRegionRectangle().isValid()) { + QPainter painter(this); + + painter.setBrush(QColor(0, 0, 0, 50)); + painter.setPen(QPen(QColor(0, 0, 0, 150), 2)); + painter.drawRect(_navigator.getZoomRegionRectangle()); + + event->accept(); + } +} + +Navigator2D::Navigator2D(Renderer2D& renderer, QObject* parent) : + QObject(parent), + _renderer(renderer), + _enabled(false), + _initialized(false), + _isNavigating(false), + _isPanning(false), + _isZooming(false), + _zoomFactor(1.0f), + _zoomMarginScreen(100.f), + _zoomMarginWorld(.0f), + _zoomRegionInProgress(false), + _userHasNavigated(), + _navigationAction(this, "Navigation") +{ +} + +void Navigator2D::initialize(QWidget* sourceWidget) +{ + Q_ASSERT(sourceWidget); + + if (!sourceWidget) + return; + + _sourceWidget = sourceWidget; + + _sourceWidget->installEventFilter(this); + _sourceWidget->setFocusPolicy(Qt::StrongFocus); + + connect(&getNavigationAction().getZoomExtentsAction(), &TriggerAction::triggered, this, [this]() -> void { + resetView(true); + }); + + const auto zoomRectangleChanged = [this]() -> void { + setZoomRectangleWorld(_navigationAction.getZoomRectangleAction().toRectF()); + }; + + zoomRectangleChanged(); + + connect(&_navigationAction.getZoomRectangleAction(), &DecimalRectangleAction::rectangleChanged, this, zoomRectangleChanged); + + connect(this, &Navigator2D::zoomRectangleWorldChanged, this, [this, zoomRectangleChanged](const QRectF& previousZoomRectangleWorld, const QRectF& currentZoomRectangleWorld) -> void { + disconnect(&_navigationAction.getZoomRectangleAction(), &DecimalRectangleAction::rectangleChanged, this, nullptr); + { + _navigationAction.getZoomExtentsAction().setEnabled(hasUserNavigated()); + + _navigationAction.getZoomRectangleAction().setRectangle(currentZoomRectangleWorld.left(), currentZoomRectangleWorld.right(), currentZoomRectangleWorld.bottom(), currentZoomRectangleWorld.top()); + } + connect(&_navigationAction.getZoomRectangleAction(), &DecimalRectangleAction::rectangleChanged, this, zoomRectangleChanged); + + _navigationAction.getZoomPercentageAction().setValue(getZoomPercentage()); + }); + + connect(this, &Navigator2D::zoomCenterWorldChanged, this, [this](const QPointF& previousZoomCenterWorld, const QPointF& currentZoomCenterWorld) -> void { + _navigationAction.getZoomCenterAction().set(currentZoomCenterWorld); + }); + + connect(this, &Navigator2D::zoomFactorChanged, this, [this](float previousZoomFactor, float currentZoomFactor) -> void { + _navigationAction.getZoomFactorAction().setValue(currentZoomFactor); + }); + + connect(&_navigationAction.getZoomCenterAction(), &DecimalPointAction::valueChanged, this, [this](float x, float y) -> void { + beginChangeZoomRectangleWorld(); + { + setZoomCenterWorld(QPointF(x, y)); + } + endChangeZoomRectangleWorld(); + }); + + connect(&_navigationAction.getZoomInAction(), &TriggerAction::triggered, this, [this]() -> void { + setZoomPercentage(getZoomPercentage() + 10.f); + }); + + connect(&_navigationAction.getZoomPercentageAction(), &DecimalAction::valueChanged, this, [this](float value) -> void { + _userHasNavigated = true; + setZoomPercentage(value); + }); + + connect(&_navigationAction.getZoomOutAction(), &TriggerAction::triggered, this, [this]() -> void { + setZoomPercentage(getZoomPercentage() - 10.f); + }); + + connect(&_navigationAction.getZoomFactorAction(), &DecimalAction::valueChanged, this, [this](float value) -> void { + setZoomFactor(value); + }); + + connect(&_renderer, &Renderer2D::worldBoundsChanged, this, [this](const QRectF& worldBounds) -> void { + if (!hasUserNavigated()) + resetView(); + }); + + connect(&_navigationAction.getZoomRegionAction(), &TriggerAction::triggered, this, [this]() -> void { + beginZoomToRegion(); + }); + + setZoomFactor(_navigationAction.getZoomFactorAction().getValue()); + + _sourceWidget->addAction(&_navigationAction.getZoomInAction()); + _sourceWidget->addAction(&_navigationAction.getZoomExtentsAction()); + _sourceWidget->addAction(&_navigationAction.getZoomOutAction()); + _sourceWidget->addAction(&_navigationAction.getZoomSelectionAction()); + _sourceWidget->addAction(&_navigationAction.getZoomRegionAction()); + + _zoomOverlayWidget = new ZoomOverlayWidget(*this, _sourceWidget); + + _initialized = true; +} + +bool Navigator2D::eventFilter(QObject* watched, QEvent* event) +{ + if (!_initialized || !_enabled) + return false; + + if (getNavigationAction().getFreezeNavigation().isChecked()) + return QObject::eventFilter(watched, event); + + if (event->type() == QEvent::KeyPress) { + if (const auto* keyEvent = dynamic_cast(event)) { + if (keyEvent->key() == Qt::Key_Alt) { + beginNavigation(); + + return QObject::eventFilter(watched, event); + } + } + } + + if (event->type() == QEvent::KeyRelease) { + if (const auto* keyEvent = dynamic_cast(event)) { + if (keyEvent->key() == Qt::Key_Alt) { + endNavigation(); + + return QObject::eventFilter(watched, event); + } + } + } + + if (isNavigating()) { + if (_zoomRegionInProgress) { + const auto updateZoomRegion = [this]() -> void { + int x1 = std::min(_zoomRegionPoints[0].x(), _zoomRegionPoints[1].x()); + int y1 = std::min(_zoomRegionPoints[0].y(), _zoomRegionPoints[1].y()); + int x2 = std::max(_zoomRegionPoints[0].x(), _zoomRegionPoints[1].x()); + int y2 = std::max(_zoomRegionPoints[0].y(), _zoomRegionPoints[1].y()); + + _zoomRegionRectangle = QRect(x1, y1, x2 - x1, y2 - y1); + + if (_zoomOverlayWidget) + _zoomOverlayWidget->update(); + }; + + if (event->type() == QEvent::MouseButtonPress) { + if (const auto* mouseEvent = dynamic_cast(event)) { + if (mouseEvent->buttons() == Qt::LeftButton) { + _zoomRegionPoints << mouseEvent->pos() << mouseEvent->pos(); + + updateZoomRegion(); + } + } + } + + if (event->type() == QEvent::MouseMove) { + if (const auto* mouseEvent = dynamic_cast(event)) { + if (mouseEvent->buttons() == Qt::LeftButton) { + if (_zoomRegionPoints.size() == 2) + _zoomRegionPoints[1] = mouseEvent->pos(); + + updateZoomRegion(); + } + } + } + + if (event->type() == QEvent::MouseButtonRelease) { + if (const auto* mouseEvent = dynamic_cast(event)) { + if (mouseEvent->button() == Qt::LeftButton) { + endZoomToRegion(); + } + } + } + } else { + if (event->type() == QEvent::Wheel) { + if (auto* wheelEvent = dynamic_cast(event)) { + constexpr auto zoomSensitivity = .1f; + + if (wheelEvent->angleDelta().x() < 0) + zoomAround(wheelEvent->position().toPoint(), 1.0f - zoomSensitivity); + else + zoomAround(wheelEvent->position().toPoint(), 1.0f + zoomSensitivity); + } + } + + if (event->type() == QEvent::MouseButtonPress) { + if (const auto* mouseEvent = dynamic_cast(event)) { + if (mouseEvent->button() == Qt::MiddleButton) + resetView(); + + if (mouseEvent->buttons() == Qt::LeftButton) { + changeCursor(Qt::ClosedHandCursor); + + _mousePositions << mouseEvent->pos(); + + _sourceWidget->update(); + } + } + } + + if (event->type() == QEvent::MouseButtonRelease) { + restoreCursor(); + + _mousePositions.clear(); + + _sourceWidget->update(); + } + + if (event->type() == QEvent::MouseMove) { + if (const auto* mouseEvent = dynamic_cast(event)) { + _mousePositions << mouseEvent->pos(); + + if (mouseEvent->buttons() == Qt::LeftButton && _mousePositions.size() >= 2) { + const auto& previousMousePosition = _mousePositions[_mousePositions.size() - 2]; + const auto& currentMousePosition = _mousePositions[_mousePositions.size() - 1]; + const auto panVector = currentMousePosition - previousMousePosition; + + panBy(-panVector); + } + } + } + } + } + + return QObject::eventFilter(watched, event); +} + +QMatrix4x4 Navigator2D::getViewMatrix() const +{ + QMatrix4x4 lookAt, scale; + + const auto zoomRectangle = getZoomRectangleWorld(); + + // Construct look-at parameters + const auto eye = QVector3D(zoomRectangle.center().x(), zoomRectangle.center().y(), 1); + const auto center = QVector3D(zoomRectangle.center().x(), zoomRectangle.center().y(), 0); + const auto up = QVector3D(0, 1, 0); + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__ << getZoomRectangleWorld() << _renderer.getDataBounds() << eye; +#endif + + // Create look-at transformation matrix + lookAt.lookAt(eye, center, up); + + const auto viewerSize = _renderer.getRenderSize(); + const auto factorX = static_cast(viewerSize.width()) / (zoomRectangle.isValid() ? static_cast(zoomRectangle.width()) : 1.0f); + const auto factorY = static_cast(viewerSize.height()) / (zoomRectangle.isValid() ? static_cast(zoomRectangle.height()) : 1.0f); + const auto scaleFactor = factorX < factorY ? factorX : factorY; + + const auto d = 1.0f - (2 * 0) / std::max(viewerSize.width(), viewerSize.height()); + + // Create scale matrix + scale.scale(scaleFactor * d, scaleFactor * d, scaleFactor * d); + + // Return composite matrix of scale and look-at transformation matrix + return scale * lookAt; +} + +QRectF Navigator2D::getZoomRectangleWorld() const +{ + const auto zoomRectangleWorldSize = _renderer.getRenderSize().toSizeF() * _zoomFactor; + const auto zoomRectangleWorldTopLeft = _zoomCenterWorld - QPointF(.5f * static_cast(zoomRectangleWorldSize.width()), .5f * static_cast(zoomRectangleWorldSize.height())); + + return { + zoomRectangleWorldTopLeft, + zoomRectangleWorldSize + }; +} + +void Navigator2D::setZoomRectangleWorld(const QRectF& zoomRectangleWorld) +{ + if (getNavigationAction().getFreezeNavigation().isChecked()) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__ << zoomRectangleWorld; +#endif + + const auto previousZoomRectangleWorld = getZoomRectangleWorld(); + + if (zoomRectangleWorld == previousZoomRectangleWorld) + return; + + _zoomFactor = std::max( + static_cast(zoomRectangleWorld.width()) / static_cast(_renderer.getRenderSize().width()), + static_cast(zoomRectangleWorld.height()) / static_cast(_renderer.getRenderSize().height()) + ); + + _zoomCenterWorld = zoomRectangleWorld.center(); + + emit zoomRectangleWorldChanged(previousZoomRectangleWorld, getZoomRectangleWorld()); +} + +float Navigator2D::getZoomMarginScreen() const +{ + return _zoomMarginScreen; +} + +float Navigator2D::getZoomFactor() const +{ + return _zoomFactor; +} + +void Navigator2D::setZoomFactor(float zoomFactor) +{ + if (getNavigationAction().getFreezeNavigation().isChecked()) + return; + + if (zoomFactor == _zoomFactor) + return; + + const auto previousZoomFactor = _zoomFactor; + + _zoomFactor = zoomFactor; + + emit zoomFactorChanged(previousZoomFactor, _zoomFactor); + + setZoomPercentage(getZoomPercentage()); +} + +float Navigator2D::getZoomPercentage() const +{ + const auto worldBounds = _renderer.getWorldBounds(); + const auto zoomRectangleWorld = getZoomRectangleWorld(); + + if (!worldBounds.isValid() || !zoomRectangleWorld.isValid()) + return 1.0f; + + const auto factorX = static_cast(worldBounds.width()) / static_cast(zoomRectangleWorld.width()); + const auto factorY = static_cast(worldBounds.height()) / static_cast(zoomRectangleWorld.height()); + const auto scaleFactor = factorX > factorY ? factorX : factorY; + + return scaleFactor * 100.f; +} + +void Navigator2D::setZoomPercentage(float zoomPercentage) +{ + if (getNavigationAction().getFreezeNavigation().isChecked()) + return; + + beginZooming(); + { + beginChangeZoomRectangleWorld(); + { + if (zoomPercentage < 0.01f) + return; + + const auto zoomPercentageNormalized = .01f * zoomPercentage; + const auto zoomFactorX = static_cast(_renderer.getWorldBounds().width()) / static_cast(_renderer.getRenderSize().width()); + const auto zoomFactorY = static_cast(_renderer.getWorldBounds().height()) / static_cast(_renderer.getRenderSize().height()); + + setZoomFactor(std::max(zoomFactorX, zoomFactorY) / zoomPercentageNormalized); + } + endChangeZoomRectangleWorld(); + } + endZooming(); +} + +bool Navigator2D::isEnabled() const +{ + return _enabled; +} + +void Navigator2D::setEnabled(bool enabled) +{ + if (enabled == _enabled) + return; + + _enabled = enabled; + + _navigationAction.setShortcutsEnabled(_enabled); + + emit enabledChanged(_enabled); +} + +gui::NavigationAction& Navigator2D::getNavigationAction() +{ + return _navigationAction; +} + +const gui::NavigationAction& Navigator2D::getNavigationAction() const +{ + return _navigationAction; +} + +void Navigator2D::zoomAround(const QPoint& center, float factor) +{ + if (getNavigationAction().getFreezeNavigation().isChecked()) + return; + + if (!_initialized) + return; + + beginZooming(); + { + beginChangeZoomRectangleWorld(); + { + const auto p1 = _renderer.getScreenPointToWorldPosition(getViewMatrix(), center).toPointF(); + + setZoomFactor(_zoomFactor /= factor); + setZoomCenterWorld(p1 + (_zoomCenterWorld - p1) / factor); + } + endChangeZoomRectangleWorld(); + } + endZooming(); +} + +void Navigator2D::zoomToRectangle(const QRectF& zoomRectangle) +{ + if (getNavigationAction().getFreezeNavigation().isChecked()) + return; + + if (!_initialized) + return; + + beginZooming(); + { + beginChangeZoomRectangleWorld(); + { + setZoomRectangleWorld(zoomRectangle); + } + endChangeZoomRectangleWorld(); + } + endZooming(); +} + +void Navigator2D::panBy(const QPointF& delta) +{ + if (getNavigationAction().getFreezeNavigation().isChecked()) + return; + + if (!_initialized) + return; + + beginPanning(); + { + beginChangeZoomRectangleWorld(); + { + const auto p1 = _renderer.getScreenPointToWorldPosition(getViewMatrix(), QPoint()).toPointF(); + const auto p2 = _renderer.getScreenPointToWorldPosition(getViewMatrix(), delta.toPoint()).toPointF(); + + setZoomCenterWorld(getZoomRectangleWorld().center() + (p2 - p1)); + } + endChangeZoomRectangleWorld(); + } + endPanning(); +} + +void Navigator2D::setZoomCenterWorld(const QPointF& zoomCenterWorld) +{ + if (getNavigationAction().getFreezeNavigation().isChecked()) + return; + + if (zoomCenterWorld == _zoomCenterWorld) + return; + + const auto previousZoomCenterWorld = _zoomCenterWorld; + + _zoomCenterWorld = zoomCenterWorld; + + emit zoomCenterWorldChanged(previousZoomCenterWorld, _zoomCenterWorld); +} + +void Navigator2D::resetView(bool force /*= false*/) +{ + if (!force && (mv::projects().isOpeningProject() || mv::projects().isImportingProject())) + return; + + if (!_initialized) + return; + + if (!force && hasUserNavigated()) + return; + + if (_navigationAction.getFreezeNavigation().isChecked()) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__ << force; +#endif + + beginZooming(); + { + beginChangeZoomRectangleWorld(); + { + const auto zoomFactorX = _renderer.getWorldBounds().width() / static_cast(_renderer.getRenderSize().width()); + const auto zoomFactorY = _renderer.getWorldBounds().height() / static_cast(_renderer.getRenderSize().height()); + + setZoomFactor(std::max(zoomFactorX, zoomFactorY)); + setZoomCenterWorld(_renderer.getWorldBounds().center()); + } + endChangeZoomRectangleWorld(); + } + endZooming(); + + _userHasNavigated = false; +} + +bool Navigator2D::isPanning() const +{ + return _isPanning; +} + +bool Navigator2D::isZooming() const +{ + return _isZooming; +} + +bool Navigator2D::isNavigating() const +{ + return _isNavigating; +} + +bool Navigator2D::hasUserNavigated() const +{ + return _userHasNavigated; +} + +QRect Navigator2D::getZoomRegionRectangle() const +{ + return _zoomRegionRectangle; +} + +void Navigator2D::setIsPanning(bool isPanning) +{ + if (!_initialized) + return; + + if (isPanning == _isPanning) + return; + + _isPanning = isPanning; + + emit isPanningChanged(_isPanning); +} + +void Navigator2D::setIsZooming(bool isZooming) +{ + if (!_initialized) + return; + + if (isZooming == _isZooming) + return; + + _isZooming = isZooming; + + emit isZoomingChanged(_isZooming); +} + +void Navigator2D::setIsNavigating(bool isNavigating) +{ + if (!_initialized) + return; + + if (isNavigating == _isNavigating) + return; + + _isNavigating = isNavigating; + + emit isNavigatingChanged(_isNavigating); +} + +void Navigator2D::beginPanning() +{ + if (!_initialized) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif + + setIsPanning(true); + + _userHasNavigated = true; + + emit panningStarted(); +} + +void Navigator2D::endPanning() +{ + if (!_initialized) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif + + setIsPanning(false); + + emit panningEnded(); +} + +void Navigator2D::beginZooming() +{ + if (!_initialized) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif + + setIsZooming(true); + + _userHasNavigated = true; + + emit zoomingStarted(); +} + +void Navigator2D::endZooming() +{ + if (!_initialized) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif + + setIsZooming(false); + + emit zoomingEnded(); +} + +void Navigator2D::beginNavigation() +{ + if (!_initialized) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif + + _userHasNavigated = true; + + changeCursor(Qt::OpenHandCursor); + + setIsNavigating(true); + + emit navigationStarted(); +} + +void Navigator2D::endNavigation() +{ + if (!_initialized) + return; + +#ifdef NAVIGATOR_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif + + changeCursor(Qt::ArrowCursor); + + setIsNavigating(false); + + emit navigationEnded(); +} + +void Navigator2D::beginChangeZoomRectangleWorld() +{ + _previousZoomRectangleWorld = getZoomRectangleWorld(); +} + +void Navigator2D::endChangeZoomRectangleWorld() +{ + emit zoomRectangleWorldChanged(_previousZoomRectangleWorld, getZoomRectangleWorld()); +} + +void Navigator2D::beginZoomToRegion() +{ + setIsNavigating(true); + + _zoomRegionInProgress = true; + _zoomRegionRectangle = QRect(); + + if (_zoomOverlayWidget) + _zoomOverlayWidget->update(); +} + +void Navigator2D::endZoomToRegion() +{ + _zoomRegionPoints.clear(); + + const auto p1 = _renderer.getScreenPointToWorldPosition(getViewMatrix(), _zoomRegionRectangle.topLeft()).toPointF(); + const auto p2 = _renderer.getScreenPointToWorldPosition(getViewMatrix(), _zoomRegionRectangle.bottomRight()).toPointF(); + + setZoomRectangleWorld(QRectF(p1, p2)); + + _zoomRegionInProgress = false; + _zoomRegionRectangle = QRect(); + + if (_zoomOverlayWidget) + _zoomOverlayWidget->update(); +} + +void Navigator2D::changeCursor(const QCursor& cursor) +{ + Q_ASSERT(_sourceWidget); + + if (!_sourceWidget) + return; + + _cachedCursor = _sourceWidget->cursor(); + + _sourceWidget->setCursor(cursor); +} + +void Navigator2D::restoreCursor() const +{ + Q_ASSERT(_sourceWidget); + + if (!_sourceWidget) + return; + + _sourceWidget->setCursor(_cachedCursor); +} +} diff --git a/ManiVault/src/renderers/Navigator2D.h b/ManiVault/src/renderers/Navigator2D.h new file mode 100644 index 000000000..1c5add717 --- /dev/null +++ b/ManiVault/src/renderers/Navigator2D.h @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#pragma once + +#include + +#include +#include +#include + +namespace mv +{ + +class Renderer2D; + +/** + * Navigator 2D class + * + * Orchestrates panning and zooming in a widget that displays 2D data using Renderer2D + * + * @author Thomas Kroes + */ +class CORE_EXPORT Navigator2D : public QObject +{ + Q_OBJECT + +public: + + /** For drawing the zoom region */ + class ZoomOverlayWidget : public gui::OverlayWidget + { + public: + + /** + * Construct a new zoom overlay widget + * @param navigator Reference to the navigator + * @param targetWidget Pointer to the target widget + */ + ZoomOverlayWidget(Navigator2D& navigator, QWidget* targetWidget); + + /** + * Override the paint event to draw the zoom regio rectangle + * @param event + */ + void paintEvent(QPaintEvent* event) override; + + private: + Navigator2D& _navigator; /** Reference to the navigator */ + }; + +public: + + /** + * Construct a new two-dimensional navigator + * + * @param renderer Reference to parent renderer + * @param parent Pointer to the parent object + */ + explicit Navigator2D(Renderer2D& renderer, QObject* parent = nullptr); + + /** + * Initializes the two-dimensional navigator with a \p sourceWidget + * @param sourceWidget Pointer to the renderer widget + */ + void initialize(QWidget* sourceWidget); + + /** + * Watch \p watched for events + * + * @param watched Watched object + * @param event Event + * @return True if the event was handled, false otherwise + */ + bool eventFilter(QObject* watched, QEvent* event) override; + + /** + * Get the view matrix + * @return View matrix + */ + QMatrix4x4 getViewMatrix() const; + + /** + * Get the world zoom rectangle + * @return Zoom rectangle in world coordinates + */ + QRectF getZoomRectangleWorld() const; + + /** + * Set the world zoom rectangle to \p zoomRectangleWorld + * @param zoomRectangleWorld Zoom rectangle in world coordinates + */ + void setZoomRectangleWorld(const QRectF& zoomRectangleWorld); + + /** + * Get the zoom rectangle margin + * @return Zoom rectangle margin + */ + float getZoomMarginScreen() const; + + /** + * Get the zoom factor + * @return Zoom factor + */ + float getZoomFactor() const; + + /** + * Set the zoom factor to \p zoomFactor + * @param zoomFactor Zoom factor + */ + void setZoomFactor(float zoomFactor); + + /** + * Get the zoom percentage + * @return Zoom percentage + */ + float getZoomPercentage() const; + + /** + * Set the zoom percentage to \p zoomPercentage + * @param zoomPercentage Zoom percentage + */ + void setZoomPercentage(float zoomPercentage); + + /** + * Get whether the navigator is enabled + * @return Boolean determining whether the navigator is enabled + */ + bool isEnabled() const; + + /** + * Set enabled to \p enabled + * @param enabled Boolean determining whether the navigator is enabled + */ + void setEnabled(bool enabled); + + /** + * Get the navigation action + * @return Reference to the navigation action + */ + gui::NavigationAction& getNavigationAction(); + + /** + * Get the navigation action + * @return Reference to the navigation action + */ + const gui::NavigationAction& getNavigationAction() const; + +public: // Navigation + + /** + * Zoom by \p factor around \p center + * @param center Point to zoom around + * @param factor Zoom factor + */ + void zoomAround(const QPoint& center, float factor); + + /** + * Zoom to \p zoomRectangle + * @param zoomRectangle Zoom to this rectangle + */ + void zoomToRectangle(const QRectF& zoomRectangle); + + /** + * Pan by \p delta + * @param delta Pan by this amount + */ + void panBy(const QPointF& delta); + + /** + * Set the zoom center in world coordinates to \p zoomCenterWorld + * @param zoomCenterWorld Zoom center in world coordinates + */ + void setZoomCenterWorld(const QPointF& zoomCenterWorld); + + /** + * Reset the view + * @param force Force reset event when the user has navigated + */ + void resetView(bool force = false); + + /** + * Get whether the renderer is panning + * @return Boolean determining whether the renderer is panning + */ + bool isPanning() const; + + /** + * Get whether the renderer is zooming + * @return Boolean determining whether the renderer is zooming + */ + bool isZooming() const; + + /** + * Get whether the renderer is navigating + * @return Boolean determining whether the renderer is navigating + */ + bool isNavigating() const; + + /** + * Get whether the user has navigated + * @return Boolean determining whether the user has navigated + */ + bool hasUserNavigated() const; + + /** + * Get the zoom region rectangle in screen coordinates + * @return Zoom region rectangle in screen coordinates + */ + QRect getZoomRegionRectangle() const; + +protected: // Navigation + + /** + * Set whether the renderer is panning to \p isPanning + * @param isPanning Boolean determining whether the renderer is panning + */ + void setIsPanning(bool isPanning); + + /** + * Set whether the renderer is zooming to \p isZooming + * @param isZooming Boolean determining whether the renderer is zooming + */ + void setIsZooming(bool isZooming); + + /** + * Set whether the renderer is navigating to \p isNavigating + * @param isNavigating Boolean determining whether the renderer is navigating + */ + void setIsNavigating(bool isNavigating); + + /** Panning has begun */ + void beginPanning(); + + /** Panning has ended */ + void endPanning(); + + /** Zooming has begun */ + void beginZooming(); + + /** Zooming has ended */ + void endZooming(); + + /** Navigation has begun */ + void beginNavigation(); + + /** Navigation has ended */ + void endNavigation(); + + /** Begin changing the zoom rectangle in world coordinates */ + void beginChangeZoomRectangleWorld(); + + /** End changing the zoom rectangle in world coordinates */ + void endChangeZoomRectangleWorld(); + + /** Begin zooming to a region */ + void beginZoomToRegion(); + + /** End zooming to a region */ + void endZoomToRegion(); + +protected: // Cursor + + /** + * Change the cursor to \p cursor + * @param cursor Cursor + */ + void changeCursor(const QCursor& cursor); + + /** Restore cached cursor */ + void restoreCursor() const; + +signals: + + /** Signals that panning has started */ + void panningStarted(); + + /** Signals that panning has ended */ + void panningEnded(); + + /** + * Signals that is panning changed to \p isPanning + * @param isPanning + */ + void isPanningChanged(bool isPanning); + + /** Signals that zooming has started */ + void zoomingStarted(); + + /** Signals that zooming has ended */ + void zoomingEnded(); + + /** + * Signals that is zooming changed to \p isZooming + * @param isZooming Boolean determining whether the renderer is zooming + */ + void isZoomingChanged(bool isZooming); + + /** Signals that navigation has started */ + void navigationStarted(); + + /** Signals that navigation has ended */ + void navigationEnded(); + + /** + * Signals that is navigating changed to \p isNavigating + * @param isNavigating Boolean determining whether the renderer is navigating + */ + void isNavigatingChanged(bool isNavigating); + + /** + * Signals that enabled changed to \p enabled + * @param enabled Boolean determining whether the navigator is enabled + */ + void enabledChanged(bool enabled); + + /** + * Signals that the zoom rectangle in world coordinates has changed from \p previousZoomRectangleWorld to \p currentZoomRectangleWorld + * @param previousZoomRectangleWorld Previous world zoom rectangle + * @param currentZoomRectangleWorld Current world zoom rectangle + */ + void zoomRectangleWorldChanged(const QRectF& previousZoomRectangleWorld, const QRectF& currentZoomRectangleWorld); + + /** + * Signals that the zoom center in world coordinates has changed from \p previousZoomCenterWorld to \p currentZoomCenterWorld + * @param previousZoomCenterWorld Previous world zoom center + * @param currentZoomCenterWorld Current world zoom center + */ + void zoomCenterWorldChanged(const QPointF& previousZoomCenterWorld, const QPointF& currentZoomCenterWorld); + + /** + * Signals that the zoom factor has changed from \p previousZoomFactor to \p currentZoomFactor + * @param previousZoomFactor Previous zoom factor + * @param currentZoomFactor Current zoom factor + */ + void zoomFactorChanged(float previousZoomFactor, float currentZoomFactor); + +private: + QPointer _sourceWidget; /** Source widget for panning and zooming */ + Renderer2D& _renderer; /** Reference to parent renderer */ + bool _enabled; /** Enabled flag */ + bool _initialized; /** Initialized flag */ + QVector _mousePositions; /** Recorded mouse positions */ + bool _isNavigating; /** Navigating flag */ + bool _isPanning; /** Panning flag */ + bool _isZooming; /** Zooming flag */ + float _zoomFactor; /** Zoom factor */ + QPointF _zoomCenterWorld; /** Zoom rectangle top-left in world coordinates */ + float _zoomMarginScreen; /** Zoom margin in screen coordinates */ + float _zoomMarginWorld; /** Zoom margin in world coordinates */ + QVector _zoomRegionPoints; /** Zoom region points */ + QRect _zoomRegionRectangle; /** Zoom region rectangle */ + bool _zoomRegionInProgress; /** Zoom region in progress flag */ + QRectF _previousZoomRectangleWorld; /** Previous world zoom rectangle */ + bool _userHasNavigated; /** Boolean determining whether the user has navigated */ + gui::NavigationAction _navigationAction; /** Navigation group action */ + QCursor _cachedCursor; /** Cached cursor */ + QPointer _zoomOverlayWidget; /** Zoom overlay widget */ +}; + +} diff --git a/ManiVault/src/renderers/PointRenderer.cpp b/ManiVault/src/renderers/PointRenderer.cpp index a2cdeb1c7..764d8e097 100644 --- a/ManiVault/src/renderers/PointRenderer.cpp +++ b/ManiVault/src/renderers/PointRenderer.cpp @@ -10,24 +10,6 @@ namespace mv { namespace gui { - namespace - { - /** - * Builds an orthographic projection matrix that transforms the given bounds - * to the range [-1, 1] in both directions. - */ - Matrix3f createProjectionMatrix(const Bounds& bounds) - { - Matrix3f m; - m.setIdentity(); - m[0] = 2 / bounds.getWidth(); - m[4] = 2 / bounds.getHeight(); - m[6] = -((bounds.getRight() + bounds.getLeft()) / bounds.getWidth()); - m[7] = -((bounds.getTop() + bounds.getBottom()) / bounds.getHeight()); - return m; - } - } - void PointArrayObject::init() { initializeOpenGLFunctions(); @@ -274,6 +256,28 @@ namespace mv _positionBuffer.destroy(); } + PointRenderer::PointRenderer(QWidget* sourceWidget, QObject* parent) : + Renderer2D(parent) + { + if(sourceWidget) + setSourceWidget(sourceWidget); + } + + void PointRenderer::setDataBounds(const QRectF& dataBounds) + { + Renderer2D::setDataBounds(dataBounds); + } + + QRectF PointRenderer::computeWorldBounds() const + { + const auto marginX = getNavigator().getZoomMarginScreen() * static_cast(getDataBounds().height()) / (static_cast(getRenderSize().height() - 2.f * getNavigator().getZoomMarginScreen())); + const auto marginY = getNavigator().getZoomMarginScreen() * static_cast(getDataBounds().width()) / (static_cast(getRenderSize().width() - 2.f * getNavigator().getZoomMarginScreen())); + const auto margin = std::max(marginX, marginY); + const auto margins = QMarginsF(margin, margin, margin, margin); + + return getDataBounds().marginsAdded(margins); + } + void PointRenderer::setData(const std::vector& positions) { _gpuPoints.setPositions(positions); @@ -328,52 +332,11 @@ namespace mv _colormap.loadFromImage(image); } - Bounds PointRenderer::getBounds() const - { - return getViewBounds(); - } - - Bounds PointRenderer::getViewBounds() const - { - return _boundsView; - } - - Bounds PointRenderer::getDataBounds() const - { - return _boundsData; - } - - void PointRenderer::setBounds(const Bounds& bounds) - { - setViewBounds(bounds); - setDataBounds(bounds); - } - - void PointRenderer::setViewBounds(const Bounds& boundsView) - { - _boundsView = boundsView; - } - - void PointRenderer::setDataBounds(const Bounds& boundsData) - { - _boundsData = boundsData; - } - - Matrix3f PointRenderer::getProjectionMatrix() const - { - return _orthoM; - } - const PointArrayObject& PointRenderer::getGpuPoints() const { return _gpuPoints; } - QSize PointRenderer::getWindowsSize() const - { - return _windowSize; - } - std::int32_t PointRenderer::getNumSelectedPoints() const { return _numSelectedPoints; @@ -471,6 +434,11 @@ namespace mv _randomizedDepthEnabled = randomizedDepth; } + void PointRenderer::initView() + { + getNavigator().resetView(true); + } + void PointRenderer::init() { initializeOpenGLFunctions(); @@ -485,66 +453,48 @@ namespace mv } } - void PointRenderer::resize(QSize renderSize) - { - int w = renderSize.width(); - int h = renderSize.height(); - - _windowSize.setWidth(w); - _windowSize.setHeight(h); - } - void PointRenderer::render() { - int w = _windowSize.width(); - int h = _windowSize.height(); - int size = w < h ? w : h; - - glViewport(w / 2 - size / 2, h / 2 - size / 2, size, size); - - // World to clip transformation - _orthoM = createProjectionMatrix(_boundsView); - - _shader.bind(); - - // Point size uniforms - bool absoluteRendering = _pointSettings._scalingMode == PointScaling::Absolute; - _shader.uniform1f("pointSize", _pointSettings._pointSize); - _shader.uniform1f("pointSizeScale", absoluteRendering ? (1.0 / size) : 1.0f / size); - - _shader.uniformMatrix3f("orthoM", _orthoM); - _shader.uniform1f("pointOpacity", _pointSettings._alpha); - _shader.uniform1i("scalarEffect", _pointEffect); - - _shader.uniform4f("dataBounds", _boundsData.getLeft(), _boundsData.getRight(), _boundsData.getBottom(), _boundsData.getTop()); - - _shader.uniform1i("selectionDisplayMode", static_cast(_selectionDisplayMode)); - _shader.uniform1f("selectionOutlineScale", _selectionOutlineScale); - _shader.uniform3f("selectionOutlineColor", _selectionOutlineColor); - _shader.uniform1i("selectionOutlineOverrideColor", _selectionOutlineOverrideColor); - _shader.uniform1f("selectionOutlineOpacity", _selectionOutlineOpacity); - _shader.uniform1i("selectionHaloEnabled", _selectionHaloEnabled); - - _shader.uniform1i("randomizedDepthEnabled", _randomizedDepthEnabled); - - _shader.uniform1i("hasHighlights", _gpuPoints.hasHighlights()); - _shader.uniform1i("hasFocusHighlights", _gpuPoints.hasFocusHighlights()); - _shader.uniform1i("hasScalars", _gpuPoints.hasColorScalars()); - _shader.uniform1i("hasColors", _gpuPoints.hasColors()); - _shader.uniform1i("hasSizes", _gpuPoints.hasSizeScalars()); - _shader.uniform1i("hasOpacities", _gpuPoints.hasOpacityScalars()); - _shader.uniform1i("numSelectedPoints", _numSelectedPoints); - - if (_gpuPoints.hasColorScalars()) - _shader.uniform3f("colorMapRange", _gpuPoints.getColorMapRange()); - - if (_colormap.isCreated() && (_pointEffect == PointEffect::Color || _pointEffect == PointEffect::Color2D)) + beginRender(); { - _colormap.bind(0); - _shader.uniform1i("colormap", 0); - } + _shader.bind(); + + const bool pointSizeAbsolute = _pointSettings._scalingMode == PointScaling::Absolute; + + _shader.uniform1f("pointSize", _pointSettings._pointSize); + _shader.uniform1i("pointSizeAbsolute", pointSizeAbsolute); + _shader.uniform2f("viewportSize", static_cast(getRenderSize().width()), static_cast(getRenderSize().height())); + _shader.uniformMatrix4f("mvp", getModelViewProjectionMatrix().data()); + _shader.uniform1f("pointOpacity", _pointSettings._alpha); + _shader.uniform1i("scalarEffect", _pointEffect); + _shader.uniform4f("dataBounds", getDataBounds().left(), getDataBounds().right(), getDataBounds().bottom(), getDataBounds().top()); + _shader.uniform1i("selectionDisplayMode", static_cast(_selectionDisplayMode)); + _shader.uniform1f("selectionOutlineScale", _selectionOutlineScale); + _shader.uniform3f("selectionOutlineColor", _selectionOutlineColor); + _shader.uniform1i("selectionOutlineOverrideColor", _selectionOutlineOverrideColor); + _shader.uniform1f("selectionOutlineOpacity", _selectionOutlineOpacity); + _shader.uniform1i("selectionHaloEnabled", _selectionHaloEnabled); + _shader.uniform1i("randomizedDepthEnabled", _randomizedDepthEnabled); + _shader.uniform1i("hasHighlights", _gpuPoints.hasHighlights()); + _shader.uniform1i("hasFocusHighlights", _gpuPoints.hasFocusHighlights()); + _shader.uniform1i("hasScalars", _gpuPoints.hasColorScalars()); + _shader.uniform1i("hasColors", _gpuPoints.hasColors()); + _shader.uniform1i("hasSizes", _gpuPoints.hasSizeScalars()); + _shader.uniform1i("hasOpacities", _gpuPoints.hasOpacityScalars()); + _shader.uniform1i("numSelectedPoints", _numSelectedPoints); + + if (_gpuPoints.hasColorScalars()) + _shader.uniform3f("colorMapRange", _gpuPoints.getColorMapRange()); + + if (_colormap.isCreated() && (_pointEffect == PointEffect::Color || _pointEffect == PointEffect::Color2D)) + { + _colormap.bind(0); + _shader.uniform1i("colormap", 0); + } - _gpuPoints.draw(); + _gpuPoints.draw(); + } + endRender(); } void PointRenderer::destroy() diff --git a/ManiVault/src/renderers/PointRenderer.h b/ManiVault/src/renderers/PointRenderer.h index 36a5e124c..223ce5cde 100644 --- a/ManiVault/src/renderers/PointRenderer.h +++ b/ManiVault/src/renderers/PointRenderer.h @@ -4,7 +4,7 @@ #pragma once -#include "Renderer.h" +#include "Renderer2D.h" #include "graphics/Bounds.h" #include "graphics/BufferObject.h" @@ -78,6 +78,7 @@ namespace mv } void draw(); + void destroy(); private: @@ -113,6 +114,8 @@ namespace mv bool _dirtySizeScalars = false; bool _dirtyOpacityScalars = false; bool _dirtyColors = false; + + BufferObject _quadBufferObject; }; struct CORE_EXPORT PointSettings @@ -127,9 +130,23 @@ namespace mv float _alpha = DEFAULT_ALPHA_VALUE; }; - class CORE_EXPORT PointRenderer : public Renderer + class CORE_EXPORT PointRenderer : public Renderer2D { public: + using Renderer2D::Renderer2D; + + PointRenderer() = default; + PointRenderer(QWidget* sourceWidget, QObject* parent = nullptr); + + /** + * Set data bounds to \p dataBounds + * @param dataBounds Data bounds + */ + void setDataBounds(const QRectF& dataBounds) override; + + /** Update the world bounds */ + QRectF computeWorldBounds() const override; + void setData(const std::vector& points); void setHighlights(const std::vector& highlights, const std::int32_t& numSelectedPoints); void setFocusHighlights(const std::vector& focusHighlights, const std::int32_t& numberOfFocusHighlights); @@ -143,28 +160,7 @@ namespace mv void setColormap(const QImage& image); - // Returns getViewBounds() - Bounds getBounds() const; - - // Retuns _boundsView - Bounds getViewBounds() const; - - // Returns _boundsData - Bounds getDataBounds() const; - - // Calls both setViewBounds() and setDataBounds() - void setBounds(const Bounds& bounds); - - // sets _boundsView, used for computing the projection matrix _orthoM - void setViewBounds(const Bounds& boundsView); - - // sets _boundsData, used for scaling the 2d _colormap - void setDataBounds(const Bounds& boundsData); - - Matrix3f getProjectionMatrix() const; - const PointArrayObject& getGpuPoints() const; - QSize getWindowsSize() const; std::int32_t getNumSelectedPoints() const; const PointSettings& getPointSettings() const; @@ -172,6 +168,8 @@ namespace mv void setAlpha(const float alpha); void setPointScaling(PointScaling scalingMode); + void initView(); + public: // Selection visualization PointSelectionDisplayMode getSelectionDisplayMode() const; @@ -196,7 +194,6 @@ namespace mv void setRandomizedDepthEnabled(bool randomizedDepth); void init() override; - void resize(QSize renderSize) override; void render() override; void destroy() override; @@ -205,7 +202,7 @@ namespace mv private: /* Point properties */ - PointSettings _pointSettings; + PointSettings _pointSettings = {}; PointEffect _pointEffect = PointEffect::Size; /** Selection visualization */ @@ -219,19 +216,10 @@ namespace mv /* Depth control */ bool _randomizedDepthEnabled = true; - /* Window properties */ - QSize _windowSize; - /* Rendering variables */ - ShaderProgram _shader; - - PointArrayObject _gpuPoints; - Texture2D _colormap; /** 2D colormap, sets point color based on point position */ - - Matrix3f _orthoM = {}; /** Projection matrix from bounds space to clip space */ - Bounds _boundsView = Bounds(-1, 1, -1, 1); /** Used for computing the projection matrix _orthoM */ - Bounds _boundsData = Bounds(-1, 1, -1, 1); /** Used for scaling the 2d _colormap */ - + ShaderProgram _shader = {}; + PointArrayObject _gpuPoints = {}; + Texture2D _colormap = {}; /** 2D colormap, sets point color based on point position */ std::int32_t _numSelectedPoints = 0; /** Number of selected (highlighted points) */ std::int32_t _numberOfFocusHighlights = 0; /** Number of focus highlights */ }; diff --git a/ManiVault/src/renderers/Renderer.h b/ManiVault/src/renderers/Renderer.h index 3f0bef283..9f0ae2b49 100644 --- a/ManiVault/src/renderers/Renderer.h +++ b/ManiVault/src/renderers/Renderer.h @@ -12,6 +12,8 @@ #include "ManiVaultGlobals.h" +#include "actions/WidgetAction.h" + #include #include @@ -19,11 +21,40 @@ namespace mv { - class CORE_EXPORT Renderer : protected QOpenGLFunctions_3_3_Core - { - virtual void init() = 0; - virtual void resize(QSize renderSize) = 0; - virtual void render() = 0; - virtual void destroy() = 0; - }; + +class CORE_EXPORT Renderer : public QObject, protected QOpenGLFunctions_3_3_Core +{ +protected: + + /** + * Construct with pointer to \p parent object + * @param parent Pointer to parent object + */ + explicit Renderer(QObject* parent = nullptr) : + QObject(parent) + { + } + + virtual void init() = 0; + virtual void resize(QSize renderSize) = 0; + + /** + * Get the render size + * @return Render size + */ + virtual QSize getRenderSize() const = 0; + + /** Begin rendering */ + virtual void beginRender() = 0; + + /** Render */ + virtual void render() = 0; + + /** End rendering */ + virtual void endRender() = 0; + + /** Destroy the renderer */ + virtual void destroy() = 0; +}; + } diff --git a/ManiVault/src/renderers/Renderer2D.cpp b/ManiVault/src/renderers/Renderer2D.cpp new file mode 100644 index 000000000..5b765219d --- /dev/null +++ b/ManiVault/src/renderers/Renderer2D.cpp @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#include "Renderer2D.h" + +#ifdef _DEBUG + //#define RENDERER_2D_VERBOSE +#endif + +//#define RENDERER_2D_VERBOSE + +namespace mv +{ + +Renderer2D::Renderer2D(QObject* parent) : + Renderer(parent), + _navigator(*this) +{ +} + +void Renderer2D::resize(QSize renderSize) +{ + _renderSize = renderSize; + + setWorldBounds(computeWorldBounds()); + + getNavigator().resetView(); +} + +QSize Renderer2D::getRenderSize() const +{ + return _renderSize; +} + +Navigator2D& Renderer2D::getNavigator() +{ + if (_customNavigator) { + return *_customNavigator; + } + + return _navigator; +} + +const Navigator2D& Renderer2D::getNavigator() const +{ + if (_customNavigator) { + return *_customNavigator; + } + + return _navigator; +} + +void Renderer2D::setSourceWidget(QWidget* sourceWidget) +{ + getNavigator().initialize(sourceWidget); +} + +QPointer Renderer2D::getCustomNavigator() const +{ + return _customNavigator; +} + +void Renderer2D::setCustomNavigator(const QPointer& customNavigator) +{ + _customNavigator = customNavigator; +} + +void Renderer2D::beginRender() +{ +#ifdef RENDERER_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif + + glViewport(0, 0, _renderSize.width(), _renderSize.height()); + + updateModelViewProjectionMatrix(); +} + +void Renderer2D::endRender() +{ +#ifdef RENDERER_2D_VERBOSE + qDebug() << __FUNCTION__; +#endif +} + +QRectF Renderer2D::getDataBounds() const +{ + return _dataBounds; +} + +void Renderer2D::setDataBounds(const QRectF& dataBounds) +{ +#ifdef RENDERER_2D_VERBOSE + qDebug() << __FUNCTION__ << dataBounds; +#endif + + const auto previousDataBounds = _dataBounds; + + if (dataBounds == _dataBounds) + return; + + _dataBounds = dataBounds; + + emit dataBoundsChanged(previousDataBounds, _dataBounds); + + setWorldBounds(computeWorldBounds()); +} + +QRectF Renderer2D::getWorldBounds() const +{ + return _worldBounds; +} + +void Renderer2D::setWorldBounds(const QRectF& worldBounds) +{ +#ifdef RENDERER_2D_VERBOSE + qDebug() << __FUNCTION__ << _worldBounds; +#endif + + const auto previousWorldBounds = _worldBounds; + + if (worldBounds == _worldBounds) + return; + + _worldBounds = worldBounds; + + emit worldBoundsChanged(previousWorldBounds, _worldBounds); +} + +void Renderer2D::updateModelViewProjectionMatrix() +{ + _modelViewProjectionMatrix = QMatrix4x4(getProjectionMatrix()) * getNavigator().getViewMatrix() * _modelMatrix; +} + +QVector3D Renderer2D::getScreenPointToWorldPosition(const QMatrix4x4& modelViewMatrix, const QPoint& screenPoint) const +{ + return QVector3D(screenPoint.x(), getRenderSize().height() - screenPoint.y(), 0).unproject(modelViewMatrix, getProjectionMatrix(), QRect(0, 0, getRenderSize().width(), getRenderSize().height())); +} + +QVector2D Renderer2D::getWorldPositionToNormalizedScreenPoint(const QVector3D& position) const +{ + const auto clipSpacePos = getProjectionMatrix() * (getNavigator().getViewMatrix() * QVector4D(position, 1.0)); + + return (clipSpacePos.toVector3D() / clipSpacePos.w()).toVector2D(); +} + +QPoint Renderer2D::getWorldPositionToScreenPoint(const QVector3D& position) const +{ + const auto normalizedScreenPoint = QVector2D(1.0f, -1.0f) * getWorldPositionToNormalizedScreenPoint(position); + const auto viewSize = QVector2D(getRenderSize().width(), getRenderSize().height()); + + return (viewSize * ((QVector2D(1.0f, 1.0f) + normalizedScreenPoint) / 2.0f)).toPoint(); +} + +QVector2D Renderer2D::getScreenPointToNormalizedScreenPoint(const QVector2D& screenPoint) const +{ + const auto viewSize = QVector2D(getRenderSize().width(), getRenderSize().height()); + + return QVector2D(-1.f, -1.f) + 2.f * (QVector2D(screenPoint.x(), getRenderSize().height() - screenPoint.y()) / viewSize); +} + +QMatrix4x4 Renderer2D::getScreenToNormalizedScreenMatrix() const +{ + QMatrix4x4 translate, scale; + + translate.translate(-1.0f, -1.0f, 0.0f); + scale.scale(2.0f / static_cast(getRenderSize().width()), 2.0f / static_cast(getRenderSize().height()), 1.0f); + + return translate * scale; +} + +QMatrix4x4 Renderer2D::getNormalizedScreenToScreenMatrix() const +{ + QMatrix4x4 translate, scale; + + const auto size = QSizeF(getRenderSize()); + const auto halfSize = 0.5f * size; + + scale.scale(halfSize.width(), halfSize.height(), 1.0f); + translate.translate(size.width(), 1, 0.0f); + + return translate * scale; +} + +float Renderer2D::getZoomPercentage() const +{ + const auto factorX = static_cast(getDataBounds().width()) / static_cast(getNavigator().getZoomRectangleWorld().width()); + const auto factorY = static_cast(getDataBounds().height()) / static_cast(getNavigator().getZoomRectangleWorld().height()); + const auto scaleFactor = factorX < factorY ? factorX : factorY; + + return scaleFactor; +} + +QMatrix4x4 Renderer2D::getProjectionMatrix() const +{ + // Compute half of the widget size + const auto halfSize = getRenderSize() / 2; + + QMatrix4x4 matrix; + + // Create an orthogonal transformation matrix + matrix.ortho(-halfSize.width(), halfSize.width(), -halfSize.height(), halfSize.height(), -1000.0f, +1000.0f); + + return matrix; +} + +QRect Renderer2D::getScreenRectangleFromWorldRectangle(const QRectF& worldBoundingRectangle) const +{ + // Compute screen bounding rectangle extremes + const auto topLeftScreen = getWorldPositionToScreenPoint(QVector3D(worldBoundingRectangle.bottomLeft())); + const auto bottomRightScreen = getWorldPositionToScreenPoint(QVector3D(worldBoundingRectangle.topRight())); + + return { + topLeftScreen, + bottomRightScreen + }; +} + +QMatrix4x4 Renderer2D::getModelMatrix() const +{ + return _modelMatrix; +} + +void Renderer2D::setModelMatrix(const QMatrix4x4& modelMatrix) +{ + _modelMatrix = modelMatrix; +} + +QMatrix4x4 Renderer2D::getModelViewProjectionMatrix() const +{ + return _modelViewProjectionMatrix; +} + +} diff --git a/ManiVault/src/renderers/Renderer2D.h b/ManiVault/src/renderers/Renderer2D.h new file mode 100644 index 000000000..9d2991848 --- /dev/null +++ b/ManiVault/src/renderers/Renderer2D.h @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#pragma once + +#include "Renderer.h" +#include "Navigator2D.h" + +#include + +namespace mv +{ + +/** + * Renderer 2D class + * + * Supports two-dimensional rendering: + * - Orchestrates panning and zooming using Navigator2D + * - Sets up the matrix transformations + * - Renders 2D data + * + * @author Thomas Kroes + */ +class CORE_EXPORT Renderer2D : public Renderer +{ + +Q_OBJECT + +public: + + /** + * Construct with pointer to \p parent object + * @param parent Pointer to parent object + */ + explicit Renderer2D(QObject* parent = nullptr); + + /** + * Resize the renderer to \p renderSize + * @param renderSize New size of the renderer + */ + void resize(QSize renderSize) override; + + /** + * Get the render size + * @return Render size + */ + QSize getRenderSize() const override; + + /** + * Get the 2D navigator + * @return Reference to the 2D navigator + */ + Navigator2D& getNavigator(); + + /** + * Get the 2D navigator + * @return Reference to the 2D navigator + */ + const Navigator2D& getNavigator() const; + + /** + * Initializes the source widget used for setting the renderer view + * @param sourceWidget Pointer to the renderer widget + */ + void setSourceWidget(QWidget* sourceWidget); + +public: + + /** + * Get custom navigator + * @return Pointer to custom navigator + */ + QPointer getCustomNavigator() const; + +/** + * Set custom navigator to \p customNavigator + * @param customNavigator Pointer to custom navigator + */ + void setCustomNavigator(const QPointer& customNavigator); + +public: // Coordinate conversions + + /** + * Convert \p screenPoint to point in world coordinates using \p modelViewMatrix + * @param modelViewMatrix Model-view matrix + * @param screenPoint Point in screen coordinates [0..width, 0..height] + * @return Position in world coordinates + */ + QVector3D getScreenPointToWorldPosition(const QMatrix4x4& modelViewMatrix, const QPoint& screenPoint) const; + + /** + * Convert \p position in world coordinates to point in normalized screen coordinates + * @param position Position in world coordinates + * @return Point in normalized screen coordinates [-1..1, -1..1] + */ + QVector2D getWorldPositionToNormalizedScreenPoint(const QVector3D& position) const; + + /** + * Convert \p position in world coordinates to point in screen coordinates + * @param position Position in world coordinates + * @return Point in screen coordinates [0..width, 0..height] + */ + QPoint getWorldPositionToScreenPoint(const QVector3D& position) const; + + /** + * Convert \p screenPoint to point in normalized screen coordinates + * @param screenPoint Point in screen coordinates [0..width, 0..height] + * @return Point in normalized screen coordinates [-1..1, -1..1] + */ + QVector2D getScreenPointToNormalizedScreenPoint(const QVector2D& screenPoint) const; + + /** Returns the matrix that converts screen coordinates [0..width, 0..height] to normalized screen coordinates [-1..1, -1..1] */ + QMatrix4x4 getScreenToNormalizedScreenMatrix() const; + + /** Returns the matrix that converts normalized screen coordinates [-1..1, -1..1] to screen coordinates [0..width, 0..height] */ + QMatrix4x4 getNormalizedScreenToScreenMatrix() const; + + float getZoomPercentage() const; + + /** Returns the projection matrix */ + QMatrix4x4 getProjectionMatrix() const; + + /** + * Get screen bounding rectangle from world bounding rectangle + * @param worldBoundingRectangle World bounding rectangle + */ + QRect getScreenRectangleFromWorldRectangle(const QRectF& worldBoundingRectangle) const; + + /** + * Get model matrix + * @return Model matrix + */ + QMatrix4x4 getModelMatrix() const; + + /** + * Set model matrix to \p modelMatrix + * @param modelMatrix Model matrix + */ + void setModelMatrix(const QMatrix4x4& modelMatrix); + + /** + * Get model-view-projection matrix + * @return Model-view-projection matrix + */ + QMatrix4x4 getModelViewProjectionMatrix() const; + +protected: + + /** Begin rendering (sets up the OpenGL viewport and computes the model-view-projection matrix) */ + void beginRender() override; + + /** End rendering */ + void endRender() override; + +public: + + /** + * Get data bounds + * @return Data bounds + */ + QRectF getDataBounds() const; + + /** + * Set data bounds to \p dataBounds + * @param dataBounds Data bounds + */ + virtual void setDataBounds(const QRectF& dataBounds); + + /** + * Get world bounds + * @return World bounds + */ + QRectF getWorldBounds() const; + + /** + * Set world bounds to \p worldBounds + * @param worldBounds World bounds + */ + virtual void setWorldBounds(const QRectF& worldBounds); + +private: + + /** Update the model-view-projection matrix */ + void updateModelViewProjectionMatrix(); + +protected: + + /** Compute the world bounds */ + virtual QRectF computeWorldBounds() const = 0; + +signals: + + /** + * Signals that the data bounds have changed from \p previousDataBounds to \p currentDataBounds + * @param previousDataBounds Previous data bounds + * @param currentDataBounds Current data bounds + */ + void dataBoundsChanged(const QRectF& previousDataBounds, const QRectF& currentDataBounds); + + /** + * Signals that the world bounds have changed from \p previousWorldBounds to \p currentWorldBounds + * @param previousWorldBounds Previous world bounds + * @param currentWorldBounds Current world bounds + */ + void worldBoundsChanged(const QRectF& previousWorldBounds, const QRectF& currentWorldBounds); + +private: + QSize _renderSize; /** Size of the renderer canvas */ + Navigator2D _navigator; /** 2D navigator */ + QPointer _customNavigator; /** Use this one in stead of Renderer2D#_navigator when set */ + QRectF _dataBounds; /** Bounds of the data */ + QRectF _worldBounds; /** Bounds of the world */ + QMatrix4x4 _modelMatrix; /** Model matrix */ + QMatrix4x4 _modelViewProjectionMatrix; /** Model-view-projection matrix */ + + friend class Navigator2D; +}; + +} diff --git a/ManiVault/src/util/DensityComputation.h b/ManiVault/src/util/DensityComputation.h index af1d283e4..4ae580e28 100644 --- a/ManiVault/src/util/DensityComputation.h +++ b/ManiVault/src/util/DensityComputation.h @@ -43,6 +43,9 @@ class CORE_EXPORT DensityComputation : protected QOpenGLFunctions_3_3_Core void compute(); + + QSize getDensityTextureSize() const { return QSize(RESOLUTION, RESOLUTION); } + private: bool hasData() const; float calculateMaxKDE(); diff --git a/ManiVault/src/util/NumericalRange.h b/ManiVault/src/util/NumericalRange.h index 408c37507..95e006857 100644 --- a/ManiVault/src/util/NumericalRange.h +++ b/ManiVault/src/util/NumericalRange.h @@ -85,6 +85,7 @@ class NumericalRange : public QPair /** * Addition operator + * @param other Other range * @return Added range */ NumericalRange& operator += (const NumericalRange& other) @@ -95,11 +96,24 @@ class NumericalRange : public QPair return *this; } + /** + * Addition operator + * @param value Value to add + * @return Added range + */ + NumericalRange& operator += (float value) + { + this->first = std::min(this->first, value); + this->second = std::max(this->second, value); + + return *this; + } + /** * Equality operator * @param rhs Right-hand-side operator */ - const bool operator == (const NumericalRange& rhs) const { + bool operator == (const NumericalRange& rhs) const { return rhs.getMinimum() == getMinimum() && rhs.getMaximum() == getMaximum(); } @@ -107,7 +121,7 @@ class NumericalRange : public QPair * Inequality operator * @param rhs Right-hand-side operator */ - const bool operator != (const NumericalRange& rhs) const { + bool operator != (const NumericalRange& rhs) const { return rhs.getMinimum() != getMinimum() || rhs.getMaximum() != getMaximum(); } }; diff --git a/ManiVault/src/util/StyledIcon.cpp b/ManiVault/src/util/StyledIcon.cpp index 07477c28f..348cb15cc 100644 --- a/ManiVault/src/util/StyledIcon.cpp +++ b/ManiVault/src/util/StyledIcon.cpp @@ -52,7 +52,7 @@ QMap StyledIcon::pixmaps = {} QVector StyledIcon::iconFontPreferenceGroups = { { "FontAwesomeRegular", "FontAwesomeSolid", "FontAwesomeBrandsRegular" } }; QMap> StyledIcon::iconFontVersions = {}; -StyledIcon::StyledIcon(const QString& iconName /*= ""*/, const QString& iconFontName /*= defaultIconFontName*/, const Version& iconFontVersion /*= defaultIconFontVersion*/, QWidget* parent /*= nullptr*/) +StyledIcon::StyledIcon(const QString& iconName /*= ""*/, const QString& iconFontName /*= defaultIconFontName*/, const Version& iconFontVersion /*= defaultIconFontVersion*/) { if (!iconName.isEmpty() && !iconFontName.isEmpty()) set(iconName, iconFontName, iconFontVersion); @@ -260,6 +260,34 @@ StyledIcon StyledIcon::withMode(const StyledIconMode& mode) return *this; } +StyledIcon StyledIcon::withModifier(const QString& iconName, const QString& iconFontName, const Version& iconFontVersion) +{ + try + { + if (iconName.isEmpty() || iconFontName.isEmpty()) { + return *this; + } + + _modifierIconName = iconName; + _modifierIconFontName = iconFontName; + _modifierIconFontVersion = iconFontVersion; + _iconSettings._modifierSha = generateSha(_modifierIconName, _modifierIconFontName, _modifierIconFontVersion); + + const auto iconFontResourcePath = getIconFontResourcePath(_modifierIconFontName, _modifierIconFontVersion); + + if (!QFile::exists(iconFontResourcePath)) + throw std::runtime_error(QString("Font resource not found: %1").arg(iconFontResourcePath).toStdString()); + + pixmaps[_iconSettings._modifierSha] = createIconPixmap(_modifierIconName, _modifierIconFontName, _modifierIconFontVersion, _iconSettings._mode == StyledIconMode::FixedColor ? Qt::black : _iconSettings._fixedColor); + } + catch (std::exception& e) + { + qWarning() << "Unable to set icon modifier: " << e.what(); + } + + return *this; +} + QFont StyledIcon::getIconFont(std::int32_t fontPointSize /*= -1*/, const QString& iconFontName /*= defaultIconFontName*/, const Version& iconFontVersion /*= defaultIconFontVersion*/) { const auto iconFontResourceName = getIconFontResourceName(iconFontName, iconFontVersion); diff --git a/ManiVault/src/util/StyledIcon.h b/ManiVault/src/util/StyledIcon.h index f31be3f16..5b0144335 100644 --- a/ManiVault/src/util/StyledIcon.h +++ b/ManiVault/src/util/StyledIcon.h @@ -39,9 +39,8 @@ class CORE_EXPORT StyledIcon * @param iconName Name of the icon * @param iconFontName Name of the icon font * @param iconFontVersion Version of the icon font - * @param parent Pointer to parent object (maybe nullptr) */ - explicit StyledIcon(const QString& iconName = "", const QString& iconFontName = defaultIconFontName, const Version& iconFontVersion = defaultIconFontVersion, QWidget* parent = nullptr); + explicit StyledIcon(const QString& iconName = "", const QString& iconFontName = defaultIconFontName, const Version& iconFontVersion = defaultIconFontVersion); /** * Copy construct from \p other styled icon @@ -150,6 +149,14 @@ class CORE_EXPORT StyledIcon */ StyledIcon withMode(const StyledIconMode& mode); + /** + * Set icon modifier + * @param iconName Name of the modifier icon + * @param iconFontName Name of the modifier icon font + * @param iconFontVersion Version of the modifier icon font + */ + StyledIcon withModifier(const QString& iconName, const QString& iconFontName = defaultIconFontName, const Version& iconFontVersion = defaultIconFontVersion); + /** * Get icon font for \p iconFontName at \p iconFontVersion * @param fontPointSize Point size of the font @@ -260,10 +267,13 @@ class CORE_EXPORT StyledIcon static void updateIconFontVersions(const QString& iconFontName); private: - QString _iconName; /** Name of the icon */ - QString _iconFontName; /** Name of the icon font */ - Version _iconFontVersion; /** Version of the icon font */ - StyledIconSettings _iconSettings; /** Icon settings */ + QString _iconName; /** Name of the icon */ + QString _iconFontName; /** Name of the icon font */ + Version _iconFontVersion; /** Version of the icon font */ + StyledIconSettings _iconSettings; /** Icon settings */ + QString _modifierIconName; /** Name of the modifier icon */ + QString _modifierIconFontName; /** Name of the modifier icon font */ + Version _modifierIconFontVersion; /** Version of the modifier icon font */ protected: static QMap fontMetadata; /** Font-specific metadata */ diff --git a/ManiVault/src/util/StyledIconCommon.h b/ManiVault/src/util/StyledIconCommon.h index 20aa6db61..a9fe8881f 100644 --- a/ManiVault/src/util/StyledIconCommon.h +++ b/ManiVault/src/util/StyledIconCommon.h @@ -42,6 +42,7 @@ struct StyledIconSettings QPalette::ColorRole getColorRoleForCurrentTheme() const; QString _sha; /** Icon key */ + QString _modifierSha; /** Modifier icon key */ StyledIconMode _mode; /** Styled icon coloring mode */ QPalette::ColorGroup _colorGroupLightTheme; /** Color group for light theme */ QPalette::ColorGroup _colorGroupDarkTheme; /** Color group for dark theme */ diff --git a/ManiVault/src/util/StyledIconEngine.cpp b/ManiVault/src/util/StyledIconEngine.cpp index f083edf6d..4291fe666 100644 --- a/ManiVault/src/util/StyledIconEngine.cpp +++ b/ManiVault/src/util/StyledIconEngine.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace mv::util { @@ -52,6 +53,19 @@ QPixmap StyledIconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::Sta break; } } + + if (!_iconSettings._modifierSha.isEmpty()) { + const auto recolorColor = qApp->palette().color(static_cast(mode), _iconSettings.getColorRoleForCurrentTheme()); + const auto scaledModifierIconPixmap = StyledIcon::pixmaps[_iconSettings._modifierSha].scaled(size / 2, Qt::AspectRatioMode::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation); + const auto recoloredModifierIconPixmap = recolorPixmap(scaledModifierIconPixmap, size / 2, recolorColor); + + QPainter modifierIconPixmapPainter(&result); + + modifierIconPixmapPainter.setRenderHint(QPainter::Antialiasing); + modifierIconPixmapPainter.setRenderHint(QPainter::SmoothPixmapTransform, true); + modifierIconPixmapPainter.setRenderHint(QPainter::LosslessImageRendering, true); + modifierIconPixmapPainter.drawPixmap(QPointF(std::round(size.width() / 2.f), std::round(size.height() / 2.f)), recoloredModifierIconPixmap); + } auto& badgeParameters = _iconSettings._badgeParameters; @@ -124,6 +138,11 @@ QPixmap StyledIconEngine::recolorPixmap(const QPixmap& pixmap, const QSize& size coloredPixmap.fill(Qt::transparent); QPainter painter(&coloredPixmap); + + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + painter.setRenderHint(QPainter::LosslessImageRendering, true); + painter.drawPixmap(0, 0, size.width(), size.height(), pixmap); painter.setCompositionMode(QPainter::CompositionMode_SourceIn);