From 18e53742b3e477383bef4bd827f9102dd7478e53 Mon Sep 17 00:00:00 2001 From: Vsevolod Hulchuk Date: Mon, 27 Jul 2026 18:45:12 -0400 Subject: [PATCH 1/3] Add legend option for field-name-only labels --- .../rviz_2d_plot_plugin/plot_2d_display.hpp | 1 + .../rviz_2d_plot_plugin/plot_2d_renderer.hpp | 2 ++ src/plot_2d_display.cpp | 8 +++++++ src/plot_2d_renderer.cpp | 22 +++++++++++++++++++ test/test_plot_2d_display_properties.cpp | 3 +++ 5 files changed, 36 insertions(+) diff --git a/include/rviz_2d_plot_plugin/plot_2d_display.hpp b/include/rviz_2d_plot_plugin/plot_2d_display.hpp index a40c842..109a7ca 100644 --- a/include/rviz_2d_plot_plugin/plot_2d_display.hpp +++ b/include/rviz_2d_plot_plugin/plot_2d_display.hpp @@ -232,6 +232,7 @@ private Q_SLOTS: rviz_common::properties::Property * legend_root_property_{nullptr}; rviz_common::properties::BoolProperty * show_legend_property_{nullptr}; rviz_common::properties::BoolProperty * show_latest_values_property_{nullptr}; + rviz_common::properties::BoolProperty * legend_field_name_only_property_{nullptr}; rviz_common::properties::EnumProperty * legend_position_property_{nullptr}; rviz_common::properties::IntProperty * legend_x_offset_property_{nullptr}; rviz_common::properties::IntProperty * legend_y_offset_property_{nullptr}; diff --git a/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp b/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp index 647e8cb..534fa14 100644 --- a/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp +++ b/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp @@ -26,6 +26,7 @@ struct RenderableSeries QColor color{80, 170, 255}; std::vector samples; std::string unit; + std::string field_name; bool enabled{true}; double line_width{2.0}; LineStyle line_style{LineStyle::Solid}; @@ -76,6 +77,7 @@ struct PlotRenderSettings int font_size{8}; bool show_legend{true}; bool show_latest_values{true}; + bool legend_field_name_only{false}; LegendPosition legend_position{LegendPosition::TopLeft}; int legend_x_offset{4}; int legend_y_offset{4}; diff --git a/src/plot_2d_display.cpp b/src/plot_2d_display.cpp index 496372a..863802b 100644 --- a/src/plot_2d_display.cpp +++ b/src/plot_2d_display.cpp @@ -241,6 +241,10 @@ Plot2DDisplay::Plot2DDisplay() show_latest_values_property_ = new rviz_common::properties::BoolProperty( "Show Values", true, "Show latest visible sample values next to legend labels.", legend_root_property_, SLOT(onRenderPropertyChanged()), this); + legend_field_name_only_property_ = new rviz_common::properties::BoolProperty( + "Field Name Only", false, + "Show only the terminal field token in legend labels (for example pose/position/x -> x).", + legend_root_property_, SLOT(onRenderPropertyChanged()), this); legend_position_property_ = new rviz_common::properties::EnumProperty( "Position", QString::fromStdString(legendPositionName(LegendPosition::TopLeft)), "Legend placement inside the plot area.", @@ -1420,6 +1424,8 @@ PlotRenderSettings Plot2DDisplay::renderSettingsFromConfig_(const Plot2DConfig & settings.show_legend = show_legend_property_ ? show_legend_property_->getBool() : true; settings.show_latest_values = show_latest_values_property_ ? show_latest_values_property_->getBool() : true; + settings.legend_field_name_only = legend_field_name_only_property_ ? + legend_field_name_only_property_->getBool() : false; settings.legend_position = legend_position_property_ ? legendPositionFromName(legend_position_property_->getStdString()) : LegendPosition::TopLeft; settings.legend_x_offset = legend_x_offset_property_ ? legend_x_offset_property_->getInt() : 4; @@ -1458,6 +1464,8 @@ std::vector Plot2DDisplay::renderableSeriesFromSnapshot_( if (series.label.empty()) { series.label = "Series"; } + series.field_name = snapshot.config.plot_mode == PlotMode::XY ? + snapshot.config.series[i].y_field : snapshot.config.series[i].field; series.unit = snapshot.config.series[i].unit; series.color = toQColor(snapshot.config.series[i].color); series.color.setAlphaF(snapshot.config.series[i].line_alpha); diff --git a/src/plot_2d_renderer.cpp b/src/plot_2d_renderer.cpp index 49caa97..45ffb66 100644 --- a/src/plot_2d_renderer.cpp +++ b/src/plot_2d_renderer.cpp @@ -203,6 +203,22 @@ Qt::PenStyle qtPenStyle(const LineStyle style) return Qt::SolidLine; } +std::string terminalFieldToken(const std::string & field_name) +{ + if (field_name.empty()) { + return {}; + } + + const std::size_t end = field_name.find_last_not_of('/'); + if (end == std::string::npos) { + return {}; + } + + const std::size_t begin = field_name.find_last_of('/', end); + const std::size_t start = begin == std::string::npos ? 0 : begin + 1; + return field_name.substr(start, end - start + 1); +} + void drawGrid( QPainter & painter, const QRectF & rect, @@ -419,6 +435,12 @@ void drawLegend( }); QString text = QString::fromStdString(item.label); + if (settings.legend_field_name_only) { + const std::string field_token = terminalFieldToken(item.field_name); + if (!field_token.empty()) { + text = QString::fromStdString(field_token); + } + } if (settings.show_latest_values && latest != item.samples.rend()) { text += " "; text += QString::fromStdString(formatPlotValue(latest->value)); diff --git a/test/test_plot_2d_display_properties.cpp b/test/test_plot_2d_display_properties.cpp index 7078c5a..2a5bb95 100644 --- a/test/test_plot_2d_display_properties.cpp +++ b/test/test_plot_2d_display_properties.cpp @@ -123,6 +123,7 @@ TEST(Plot2DDisplay, CreatesMvpPropertyLayout) ASSERT_NE(nullptr, legend); EXPECT_NE(nullptr, findChild(legend, "Enabled")); EXPECT_NE(nullptr, findChild(legend, "Show Values")); + EXPECT_NE(nullptr, findChild(legend, "Field Name Only")); EXPECT_NE(nullptr, findChild(legend, "Position")); EXPECT_NE(nullptr, findChild(legend, "X Offset")); EXPECT_NE(nullptr, findChild(legend, "Y Offset")); @@ -480,6 +481,7 @@ TEST(Plot2DDisplay, MapsLegendPropertiesToRenderSettings) ASSERT_NE(nullptr, legend); findChild(legend, "Enabled")->setValue(false); findChild(legend, "Show Values")->setValue(false); + findChild(legend, "Field Name Only")->setValue(true); findChild(legend, "Position")->setValue("Bottom Right"); findChild(legend, "X Offset")->setValue(12); findChild(legend, "Y Offset")->setValue(8); @@ -487,6 +489,7 @@ TEST(Plot2DDisplay, MapsLegendPropertiesToRenderSettings) const auto settings = Plot2DDisplayTestAccessor::renderSettingsFromProperties(display); EXPECT_FALSE(settings.show_legend); EXPECT_FALSE(settings.show_latest_values); + EXPECT_TRUE(settings.legend_field_name_only); EXPECT_EQ(settings.legend_position, LegendPosition::BottomRight); EXPECT_EQ(settings.legend_x_offset, 12); EXPECT_EQ(settings.legend_y_offset, 8); From b927a4e923e5222092edcd1e4eec0e58b4272589 Mon Sep 17 00:00:00 2001 From: Vsevolod Hulchuk Date: Tue, 28 Jul 2026 13:35:51 -0400 Subject: [PATCH 2/3] feat: add support for dual y-axes with configurable right axis --- .../rviz_2d_plot_plugin/plot_2d_config.hpp | 8 + .../rviz_2d_plot_plugin/plot_2d_display.hpp | 12 + .../rviz_2d_plot_plugin/plot_2d_renderer.hpp | 11 + src/plot_2d_config.cpp | 1 + src/plot_2d_display.cpp | 70 ++++ src/plot_2d_display_options.cpp | 28 ++ src/plot_2d_display_options.hpp | 3 + src/plot_2d_renderer.cpp | 315 ++++++++++++++---- test/plot_2d_display_test_helpers.hpp | 5 + test/test_plot_2d_config.cpp | 18 + test/test_plot_2d_display_properties.cpp | 55 +++ test/test_plot_2d_renderer.cpp | 126 +++++++ 12 files changed, 596 insertions(+), 56 deletions(-) diff --git a/include/rviz_2d_plot_plugin/plot_2d_config.hpp b/include/rviz_2d_plot_plugin/plot_2d_config.hpp index 2a84775..253deda 100644 --- a/include/rviz_2d_plot_plugin/plot_2d_config.hpp +++ b/include/rviz_2d_plot_plugin/plot_2d_config.hpp @@ -61,6 +61,12 @@ enum class XYAxisScaleMode Equal, }; +enum class SeriesAxis +{ + Left, + Right, +}; + enum class TimeSource { ReceiveTime, @@ -170,6 +176,7 @@ struct SeriesColor struct SeriesConfig { bool enabled{true}; + SeriesAxis axis{SeriesAxis::Left}; std::string topic; std::string x_field; std::string y_field; @@ -210,6 +217,7 @@ struct Plot2DConfig PlotMode plot_mode{PlotMode::TimeSeries}; XAxisConfig x_axis; AxisConfig y_axis; + AxisConfig y_axis_right; TimeConfig time; QoSConfig qos; LayoutConfig layout; diff --git a/include/rviz_2d_plot_plugin/plot_2d_display.hpp b/include/rviz_2d_plot_plugin/plot_2d_display.hpp index 109a7ca..68a876b 100644 --- a/include/rviz_2d_plot_plugin/plot_2d_display.hpp +++ b/include/rviz_2d_plot_plugin/plot_2d_display.hpp @@ -99,6 +99,7 @@ private Q_SLOTS: rviz_common::properties::EditableEnumProperty * x_field{nullptr}; rviz_common::properties::EditableEnumProperty * y_field{nullptr}; rviz_common::properties::EditableEnumProperty * field{nullptr}; + rviz_common::properties::EnumProperty * axis{nullptr}; rviz_common::properties::StringProperty * label{nullptr}; rviz_common::properties::StringProperty * unit{nullptr}; rviz_common::properties::ColorProperty * color{nullptr}; @@ -214,6 +215,10 @@ private Q_SLOTS: rviz_common::properties::BoolProperty * auto_scale_property_{nullptr}; rviz_common::properties::FloatProperty * y_min_property_{nullptr}; rviz_common::properties::FloatProperty * y_max_property_{nullptr}; + rviz_common::properties::Property * right_y_axis_root_property_{nullptr}; + rviz_common::properties::BoolProperty * right_y_auto_scale_property_{nullptr}; + rviz_common::properties::FloatProperty * right_y_min_property_{nullptr}; + rviz_common::properties::FloatProperty * right_y_max_property_{nullptr}; rviz_common::properties::Property * grid_root_property_{nullptr}; rviz_common::properties::BoolProperty * show_major_grid_property_{nullptr}; rviz_common::properties::BoolProperty * show_minor_grid_property_{nullptr}; @@ -236,6 +241,13 @@ private Q_SLOTS: rviz_common::properties::EnumProperty * legend_position_property_{nullptr}; rviz_common::properties::IntProperty * legend_x_offset_property_{nullptr}; rviz_common::properties::IntProperty * legend_y_offset_property_{nullptr}; + rviz_common::properties::Property * right_legend_root_property_{nullptr}; + rviz_common::properties::BoolProperty * show_right_legend_property_{nullptr}; + rviz_common::properties::BoolProperty * merge_right_legend_with_left_property_{nullptr}; + rviz_common::properties::BoolProperty * show_right_latest_values_property_{nullptr}; + rviz_common::properties::EnumProperty * right_legend_position_property_{nullptr}; + rviz_common::properties::IntProperty * right_legend_x_offset_property_{nullptr}; + rviz_common::properties::IntProperty * right_legend_y_offset_property_{nullptr}; rviz_common::properties::Property * layout_root_property_{nullptr}; rviz_common::properties::IntProperty * width_property_{nullptr}; rviz_common::properties::IntProperty * height_property_{nullptr}; diff --git a/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp b/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp index 534fa14..3c065fc 100644 --- a/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp +++ b/include/rviz_2d_plot_plugin/plot_2d_renderer.hpp @@ -27,6 +27,7 @@ struct RenderableSeries std::vector samples; std::string unit; std::string field_name; + SeriesAxis axis{SeriesAxis::Left}; bool enabled{true}; double line_width{2.0}; LineStyle line_style{LineStyle::Solid}; @@ -70,6 +71,10 @@ struct PlotRenderSettings double fixed_y_min{-1.0}; double fixed_y_max{1.0}; double y_padding_fraction{0.08}; + AxisScaleMode right_y_scale_mode{AxisScaleMode::Auto}; + double fixed_right_y_min{-1.0}; + double fixed_right_y_max{1.0}; + double right_y_padding_fraction{0.08}; QColor background_color{0, 0, 0, 190}; QColor axis_color{230, 230, 230, 230}; QColor grid_color{130, 130, 130, 80}; @@ -81,6 +86,12 @@ struct PlotRenderSettings LegendPosition legend_position{LegendPosition::TopLeft}; int legend_x_offset{4}; int legend_y_offset{4}; + bool show_right_legend{true}; + bool merge_right_legend_with_left{false}; + bool show_right_latest_values{true}; + LegendPosition right_legend_position{LegendPosition::TopRight}; + int right_legend_x_offset{4}; + int right_legend_y_offset{4}; bool show_major_grid{true}; bool show_minor_grid{true}; int x_major_tick_count{6}; diff --git a/src/plot_2d_config.cpp b/src/plot_2d_config.cpp index c89c715..515db8f 100644 --- a/src/plot_2d_config.cpp +++ b/src/plot_2d_config.cpp @@ -130,6 +130,7 @@ void Plot2DConfig::repair() } y_axis.repairFixedRange(); + y_axis_right.repairFixedRange(); x_axis.repairFixedRange(); time.repair(); qos.repair(); diff --git a/src/plot_2d_display.cpp b/src/plot_2d_display.cpp index 863802b..929fdbd 100644 --- a/src/plot_2d_display.cpp +++ b/src/plot_2d_display.cpp @@ -184,6 +184,18 @@ Plot2DDisplay::Plot2DDisplay() "Y Max", 1.0F, "Fixed y-axis maximum when auto scale is disabled.", y_axis_root_property_, SLOT(onRenderPropertyChanged()), this); + right_y_axis_root_property_ = new rviz_common::properties::Property( + "Right Y Axis", QVariant(), "Secondary right-side vertical axis scaling.", this); + right_y_auto_scale_property_ = new rviz_common::properties::BoolProperty( + "Auto Scale", true, "Automatically fit the right y-axis to visible samples.", + right_y_axis_root_property_, SLOT(onRenderPropertyChanged()), this); + right_y_min_property_ = new rviz_common::properties::FloatProperty( + "Y Min", -1.0F, "Fixed right y-axis minimum when auto scale is disabled.", + right_y_axis_root_property_, SLOT(onRenderPropertyChanged()), this); + right_y_max_property_ = new rviz_common::properties::FloatProperty( + "Y Max", 1.0F, "Fixed right y-axis maximum when auto scale is disabled.", + right_y_axis_root_property_, SLOT(onRenderPropertyChanged()), this); + grid_root_property_ = new rviz_common::properties::Property( "Grid", QVariant(), "Plot grid density and visibility.", this); show_major_grid_property_ = new rviz_common::properties::BoolProperty( @@ -258,6 +270,35 @@ Plot2DDisplay::Plot2DDisplay() "Y Offset", 4, "Vertical legend inset in pixels.", legend_root_property_, SLOT(onRenderPropertyChanged()), this); legend_y_offset_property_->setMin(0); + right_legend_root_property_ = new rviz_common::properties::Property( + "Right Legend", QVariant(), + "Secondary legend for right-axis series.", + legend_root_property_); + show_right_legend_property_ = new rviz_common::properties::BoolProperty( + "Enabled", true, + "Show the right-axis legend.", + right_legend_root_property_, SLOT(onRenderPropertyChanged()), this); + merge_right_legend_with_left_property_ = new rviz_common::properties::BoolProperty( + "Merge With Left", false, + "Render right-axis series entries in the main legend.", + right_legend_root_property_, SLOT(onRenderPropertyChanged()), this); + show_right_latest_values_property_ = new rviz_common::properties::BoolProperty( + "Show Values", true, + "Show latest visible sample values next to right-axis legend labels.", + right_legend_root_property_, SLOT(onRenderPropertyChanged()), this); + right_legend_position_property_ = new rviz_common::properties::EnumProperty( + "Position", QString::fromStdString(legendPositionName(LegendPosition::TopRight)), + "Right legend placement inside the plot area.", + right_legend_root_property_, SLOT(onRenderPropertyChanged()), this); + addLegendPositionOptions(right_legend_position_property_); + right_legend_x_offset_property_ = new rviz_common::properties::IntProperty( + "X Offset", 4, "Horizontal right legend inset in pixels.", + right_legend_root_property_, SLOT(onRenderPropertyChanged()), this); + right_legend_x_offset_property_->setMin(0); + right_legend_y_offset_property_ = new rviz_common::properties::IntProperty( + "Y Offset", 4, "Vertical right legend inset in pixels.", + right_legend_root_property_, SLOT(onRenderPropertyChanged()), this); + right_legend_y_offset_property_->setMin(0); layout_root_property_ = new rviz_common::properties::Property( "Layout", QVariant(), "Overlay size and screen position.", this); @@ -682,6 +723,8 @@ std::vector Plot2DDisplay::seriesConfigFromProperties_() const config.x_field = properties.x_field ? properties.x_field->getStdString() : ""; config.y_field = properties.y_field ? properties.y_field->getStdString() : ""; config.field = properties.field ? properties.field->getStdString() : ""; + config.axis = properties.axis ? + seriesAxisFromName(properties.axis->getStdString()) : SeriesAxis::Left; config.label = properties.label ? properties.label->getStdString() : "Series"; config.unit = properties.unit ? properties.unit->getStdString() : ""; config.color = properties.color ? toSeriesColor(properties.color->getColor()) : @@ -758,6 +801,10 @@ Plot2DConfig Plot2DDisplay::configFromProperties_() const AxisScaleMode::Auto : AxisScaleMode::Fixed; config.y_axis.fixed_min = y_min_property_->getFloat(); config.y_axis.fixed_max = y_max_property_->getFloat(); + config.y_axis_right.scale_mode = right_y_auto_scale_property_->getBool() ? + AxisScaleMode::Auto : AxisScaleMode::Fixed; + config.y_axis_right.fixed_min = right_y_min_property_->getFloat(); + config.y_axis_right.fixed_max = right_y_max_property_->getFloat(); config.layout.width = width_property_->getInt(); config.layout.height = height_property_->getInt(); @@ -825,6 +872,11 @@ Plot2DDisplay::SeriesPropertySet Plot2DDisplay::makeSeriesPropertySet_( &rviz_common::properties::EditableEnumProperty::requestOptions, this, &Plot2DDisplay::onFieldOptionsRequested); + properties.axis = new rviz_common::properties::EnumProperty( + "Axis", QString::fromStdString(seriesAxisName(value.axis)), + "Select whether this series uses the left or right y-axis.", + properties.root, SLOT(onConfigPropertyChanged()), this); + addSeriesAxisOptions(properties.axis); properties.label = new rviz_common::properties::StringProperty( "Label", QString::fromStdString(value.label), "Legend label for this series.", properties.root, SLOT(onSeriesAppearancePropertyChanged()), this); @@ -1410,6 +1462,10 @@ PlotRenderSettings Plot2DDisplay::renderSettingsFromConfig_(const Plot2DConfig & settings.fixed_y_min = config.y_axis.fixed_min; settings.fixed_y_max = config.y_axis.fixed_max; settings.y_padding_fraction = config.y_axis.padding_fraction; + settings.right_y_scale_mode = config.y_axis_right.scale_mode; + settings.fixed_right_y_min = config.y_axis_right.fixed_min; + settings.fixed_right_y_max = config.y_axis_right.fixed_max; + settings.right_y_padding_fraction = config.y_axis_right.padding_fraction; settings.background_color = background_color_property_->getColor(); const float background_alpha = background_alpha_property_ ? background_alpha_property_->getFloat() : 190.0F / 255.0F; @@ -1430,6 +1486,19 @@ PlotRenderSettings Plot2DDisplay::renderSettingsFromConfig_(const Plot2DConfig & legendPositionFromName(legend_position_property_->getStdString()) : LegendPosition::TopLeft; settings.legend_x_offset = legend_x_offset_property_ ? legend_x_offset_property_->getInt() : 4; settings.legend_y_offset = legend_y_offset_property_ ? legend_y_offset_property_->getInt() : 4; + settings.show_right_legend = show_right_legend_property_ ? + show_right_legend_property_->getBool() : true; + settings.merge_right_legend_with_left = merge_right_legend_with_left_property_ ? + merge_right_legend_with_left_property_->getBool() : false; + settings.show_right_latest_values = show_right_latest_values_property_ ? + show_right_latest_values_property_->getBool() : true; + settings.right_legend_position = right_legend_position_property_ ? + legendPositionFromName(right_legend_position_property_->getStdString()) : + LegendPosition::TopRight; + settings.right_legend_x_offset = right_legend_x_offset_property_ ? + right_legend_x_offset_property_->getInt() : 4; + settings.right_legend_y_offset = right_legend_y_offset_property_ ? + right_legend_y_offset_property_->getInt() : 4; settings.show_major_grid = show_major_grid_property_ ? show_major_grid_property_->getBool() : true; settings.show_minor_grid = show_minor_grid_property_ ? @@ -1466,6 +1535,7 @@ std::vector Plot2DDisplay::renderableSeriesFromSnapshot_( } series.field_name = snapshot.config.plot_mode == PlotMode::XY ? snapshot.config.series[i].y_field : snapshot.config.series[i].field; + series.axis = snapshot.config.series[i].axis; series.unit = snapshot.config.series[i].unit; series.color = toQColor(snapshot.config.series[i].color); series.color.setAlphaF(snapshot.config.series[i].line_alpha); diff --git a/src/plot_2d_display_options.cpp b/src/plot_2d_display_options.cpp index 94ab18b..3c2fb04 100644 --- a/src/plot_2d_display_options.cpp +++ b/src/plot_2d_display_options.cpp @@ -248,6 +248,34 @@ std::string seriesDefaultLabel(const SeriesConfig & series, const PlotMode plot_ return series.topic + separator + series.field; } +std::string seriesAxisName(const SeriesAxis axis) +{ + switch (axis) { + case SeriesAxis::Left: + return "Left"; + case SeriesAxis::Right: + return "Right"; + } + return "Left"; +} + +SeriesAxis seriesAxisFromName(const std::string & name) +{ + if (name == "Right") { + return SeriesAxis::Right; + } + return SeriesAxis::Left; +} + +void addSeriesAxisOptions(rviz_common::properties::EnumProperty * property) +{ + if (!property) { + return; + } + property->addOptionStd(seriesAxisName(SeriesAxis::Left)); + property->addOptionStd(seriesAxisName(SeriesAxis::Right)); +} + std::string xyHistoryModeName(const XYHistoryMode mode) { switch (mode) { diff --git a/src/plot_2d_display_options.hpp b/src/plot_2d_display_options.hpp index c0e54bb..347c973 100644 --- a/src/plot_2d_display_options.hpp +++ b/src/plot_2d_display_options.hpp @@ -55,6 +55,9 @@ std::string plotModeName(PlotMode mode); PlotMode plotModeFromName(const std::string & name); void addPlotModeOptions(rviz_common::properties::EnumProperty * property); std::string seriesDefaultLabel(const SeriesConfig & series, PlotMode plot_mode); +std::string seriesAxisName(SeriesAxis axis); +SeriesAxis seriesAxisFromName(const std::string & name); +void addSeriesAxisOptions(rviz_common::properties::EnumProperty * property); std::string xyHistoryModeName(XYHistoryMode mode); XYHistoryMode xyHistoryModeFromName(const std::string & name); diff --git a/src/plot_2d_renderer.cpp b/src/plot_2d_renderer.cpp index 45ffb66..2db6e6b 100644 --- a/src/plot_2d_renderer.cpp +++ b/src/plot_2d_renderer.cpp @@ -45,12 +45,12 @@ double majorTickStep(const std::vector & ticks) } double yAxisLabelWidth( - const PlotRenderSettings & settings, const PlotRange & y_range, + const int major_tick_count, const QFontMetrics & font_metrics) { const std::size_t y_major_count = static_cast( - std::clamp(settings.y_major_tick_count, 2, 20)); + std::clamp(major_tick_count, 2, 20)); const TickSet ticks = generateTicks(y_range, y_major_count, 0); const double step = majorTickStep(ticks.major); @@ -63,17 +63,42 @@ double yAxisLabelWidth( return static_cast(width); } -QRectF plotRect( +double leftYAxisLabelWidth( const PlotRenderSettings & settings, const PlotRange & y_range, const QFontMetrics & font_metrics) +{ + return yAxisLabelWidth(y_range, settings.y_major_tick_count, font_metrics); +} + +double rightYAxisLabelWidth( + const PlotRenderSettings & settings, + const PlotRange & right_y_range, + const bool has_right_axis, + const QFontMetrics & font_metrics) +{ + if (!has_right_axis) { + return 0.0; + } + return yAxisLabelWidth(right_y_range, settings.y_major_tick_count, font_metrics); +} + +QRectF plotRect( + const PlotRenderSettings & settings, + const PlotRange & left_y_range, + const PlotRange & right_y_range, + const bool has_right_axis, + const QFontMetrics & font_metrics) { const double left_margin = std::clamp( - yAxisLabelWidth(settings, y_range, font_metrics) + 8.0, + leftYAxisLabelWidth(settings, left_y_range, font_metrics) + 8.0, 26.0, 60.0); const double top_margin = std::max(12.0, static_cast(font_metrics.height())); - const double right_margin = 12.0; + const double right_margin = std::clamp( + rightYAxisLabelWidth(settings, right_y_range, has_right_axis, font_metrics) + 8.0, + 12.0, + 60.0); const double bottom_margin = std::max(22.0, static_cast(font_metrics.height()) + 8.0); return QRectF( @@ -124,14 +149,50 @@ std::vector visibleSamples( return samples; } -PlotRange yRangeForSettings( - const PlotRenderSettings & settings, +std::vector visibleSamplesForAxis( + const std::vector & series, + const PlotRange & time_range, + const XAxisMode x_axis_mode, + const SeriesAxis axis) +{ + std::vector samples; + for (const RenderableSeries & item : series) { + if (!item.enabled || item.axis != axis) { + continue; + } + for (const PlotSample & sample : item.samples) { + if (x_axis_mode == XAxisMode::Field || + (sample.time >= time_range.min && sample.time <= time_range.max)) + { + samples.push_back(sample); + } + } + } + return samples; +} + +bool hasEnabledSeriesOnAxis( + const std::vector & series, + const SeriesAxis axis) +{ + return std::any_of( + series.begin(), series.end(), + [axis](const RenderableSeries & item) { + return item.enabled && item.axis == axis; + }); +} + +PlotRange yRangeForAxis( + const AxisScaleMode scale_mode, + const double fixed_min, + const double fixed_max, + const double padding_fraction, const std::vector & samples) { - if (settings.y_scale_mode == AxisScaleMode::Fixed) { - return makeFixedRange(settings.fixed_y_min, settings.fixed_y_max); + if (scale_mode == AxisScaleMode::Fixed) { + return makeFixedRange(fixed_min, fixed_max); } - return makeAutoRange(samples, settings.y_padding_fraction); + return makeAutoRange(samples, padding_fraction); } PlotRange xRangeForSettings( @@ -223,7 +284,9 @@ void drawGrid( QPainter & painter, const QRectF & rect, const PlotRange & x_range, - const PlotRange & y_range, + const PlotRange & left_y_range, + const PlotRange & right_y_range, + const bool has_right_axis, const PlotRenderSettings & settings) { const std::size_t x_major_count = static_cast( @@ -235,14 +298,16 @@ void drawGrid( const TickSet x_ticks = settings.x_axis_mode == XAxisMode::Time ? generateTicks(PlotRange{0.0, settings.window_seconds}, x_major_count, minor_divisions) : generateTicks(x_range, x_major_count, minor_divisions); - const TickSet y_ticks = generateTicks(y_range, y_major_count, minor_divisions); + const TickSet left_y_ticks = generateTicks(left_y_range, y_major_count, minor_divisions); + const TickSet right_y_ticks = generateTicks(right_y_range, y_major_count, minor_divisions); const double x_step = majorTickStep(x_ticks.major); - const double y_step = majorTickStep(y_ticks.major); + const double left_y_step = majorTickStep(left_y_ticks.major); + const double right_y_step = majorTickStep(right_y_ticks.major); if (settings.show_minor_grid) { painter.setPen(QPen(scaledAlpha(settings.grid_color, 0.45), 1.0)); - for (const double tick : y_ticks.minor) { - const double y = mapY(rect, y_range, tick); + for (const double tick : left_y_ticks.minor) { + const double y = mapY(rect, left_y_range, tick); painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y)); } for (const double tick : x_ticks.minor) { @@ -255,8 +320,8 @@ void drawGrid( if (settings.show_major_grid) { painter.setPen(QPen(settings.grid_color, 1.0)); - for (const double tick : y_ticks.major) { - const double y = mapY(rect, y_range, tick); + for (const double tick : left_y_ticks.major) { + const double y = mapY(rect, left_y_range, tick); painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y)); } for (const double tick : x_ticks.major) { @@ -271,12 +336,22 @@ void drawGrid( painter.drawRect(rect); painter.setPen(QPen(settings.text_color, 1.0)); - for (const double tick : y_ticks.major) { - const double y = mapY(rect, y_range, tick); + for (const double tick : left_y_ticks.major) { + const double y = mapY(rect, left_y_range, tick); painter.drawText( QRectF(2.0, y - 8.0, rect.left() - 6.0, 16.0), Qt::AlignRight | Qt::AlignVCenter, - QString::fromStdString(formatAxisTickValue(tick, y_step))); + QString::fromStdString(formatAxisTickValue(tick, left_y_step))); + } + + if (has_right_axis) { + for (const double tick : right_y_ticks.major) { + const double y = mapY(rect, right_y_range, tick); + painter.drawText( + QRectF(rect.right() + 6.0, y - 8.0, settings.width - rect.right() - 8.0, 16.0), + Qt::AlignLeft | Qt::AlignVCenter, + QString::fromStdString(formatAxisTickValue(tick, right_y_step))); + } } for (const double tick : x_ticks.major) { @@ -400,28 +475,24 @@ void drawReferences( } } -void drawLegend( - QPainter & painter, - const QRectF & rect, +struct LegendEntry +{ + QColor color; + QString text; +}; + +void appendLegendEntries( const std::vector & series, const PlotRange & x_range, - const PlotRenderSettings & settings) + const PlotRenderSettings & settings, + const bool filter_axis, + const SeriesAxis axis, + const bool show_values, + std::vector & entries, + const QString & axis_prefix = "") { - if (!settings.show_legend) { - return; - } - - struct LegendEntry - { - QColor color; - QString text; - }; - - std::vector entries; - entries.reserve(series.size()); - double text_width = 0.0; for (const RenderableSeries & item : series) { - if (!item.enabled) { + if (!item.enabled || (filter_axis && item.axis != axis)) { continue; } @@ -441,7 +512,10 @@ void drawLegend( text = QString::fromStdString(field_token); } } - if (settings.show_latest_values && latest != item.samples.rend()) { + if (!axis_prefix.isEmpty()) { + text = axis_prefix + " " + text; + } + if (show_values && latest != item.samples.rend()) { text += " "; text += QString::fromStdString(formatPlotValue(latest->value)); if (!item.unit.empty()) { @@ -449,29 +523,43 @@ void drawLegend( text += QString::fromStdString(item.unit); } } - text_width = std::max( - text_width, - static_cast(painter.fontMetrics().horizontalAdvance(text))); entries.push_back(LegendEntry{item.color, text}); } +} +void drawLegendEntries( + QPainter & painter, + const QRectF & rect, + const PlotRenderSettings & settings, + const std::vector & entries, + const LegendPosition position, + const int x_offset_px, + const int y_offset_px) +{ if (entries.empty()) { return; } + double text_width = 0.0; + for (const LegendEntry & entry : entries) { + text_width = std::max( + text_width, + static_cast(painter.fontMetrics().horizontalAdvance(entry.text))); + } + const double line_height = std::max( 14.0, static_cast(painter.fontMetrics().height()) + 2.0); const double legend_width = std::min(rect.width() - 8.0, std::max(72.0, text_width + 24.0)); const double legend_height = line_height * static_cast(entries.size()); const bool align_right = - settings.legend_position == LegendPosition::TopRight || - settings.legend_position == LegendPosition::BottomRight; + position == LegendPosition::TopRight || + position == LegendPosition::BottomRight; const bool align_bottom = - settings.legend_position == LegendPosition::BottomLeft || - settings.legend_position == LegendPosition::BottomRight; - const double x_offset = static_cast(std::max(0, settings.legend_x_offset)); - const double y_offset = static_cast(std::max(0, settings.legend_y_offset)); + position == LegendPosition::BottomLeft || + position == LegendPosition::BottomRight; + const double x_offset = static_cast(std::max(0, x_offset_px)); + const double y_offset = static_cast(std::max(0, y_offset_px)); const double x = align_right ? rect.right() - legend_width - x_offset : rect.left() + x_offset; double y = align_bottom ? rect.bottom() - legend_height - y_offset : rect.top() + y_offset; @@ -489,6 +577,91 @@ void drawLegend( } } +void drawLegend( + QPainter & painter, + const QRectF & rect, + const std::vector & series, + const PlotRange & x_range, + const PlotRenderSettings & settings) +{ + if (!settings.show_legend) { + return; + } + + std::vector left_entries; + std::vector right_entries; + left_entries.reserve(series.size()); + right_entries.reserve(series.size()); + + appendLegendEntries( + series, + x_range, + settings, + true, + SeriesAxis::Left, + settings.show_latest_values, + left_entries); + if (settings.show_right_legend) { + appendLegendEntries( + series, + x_range, + settings, + true, + SeriesAxis::Right, + settings.show_right_latest_values, + right_entries); + } + + if (settings.merge_right_legend_with_left && settings.show_right_legend) { + std::vector merged_entries; + merged_entries.reserve(left_entries.size() + right_entries.size()); + appendLegendEntries( + series, + x_range, + settings, + true, + SeriesAxis::Left, + settings.show_latest_values, + merged_entries, + "[L]"); + appendLegendEntries( + series, + x_range, + settings, + true, + SeriesAxis::Right, + settings.show_right_latest_values, + merged_entries, + "[R]"); + drawLegendEntries( + painter, + rect, + settings, + merged_entries, + settings.legend_position, + settings.legend_x_offset, + settings.legend_y_offset); + return; + } + + drawLegendEntries( + painter, + rect, + settings, + left_entries, + settings.legend_position, + settings.legend_x_offset, + settings.legend_y_offset); + drawLegendEntries( + painter, + rect, + settings, + right_entries, + settings.right_legend_position, + settings.right_legend_x_offset, + settings.right_legend_y_offset); +} + } // namespace QImage Plot2DRenderer::render( @@ -511,25 +684,55 @@ QImage Plot2DRenderer::render( settings.now}; const std::vector samples = visibleSamples( series, time_range, settings.x_axis_mode); + const std::vector left_axis_samples = visibleSamplesForAxis( + series, time_range, settings.x_axis_mode, SeriesAxis::Left); + const std::vector right_axis_samples = visibleSamplesForAxis( + series, time_range, settings.x_axis_mode, SeriesAxis::Right); + const bool has_right_axis = hasEnabledSeriesOnAxis(series, SeriesAxis::Right); PlotRange x_range = xRangeForSettings(settings, samples); - PlotRange y_range = yRangeForSettings(settings, samples); + PlotRange left_y_range = yRangeForAxis( + settings.y_scale_mode, + settings.fixed_y_min, + settings.fixed_y_max, + settings.y_padding_fraction, + left_axis_samples.empty() ? samples : left_axis_samples); + PlotRange right_y_range = has_right_axis ? + yRangeForAxis( + settings.right_y_scale_mode, + settings.fixed_right_y_min, + settings.fixed_right_y_max, + settings.right_y_padding_fraction, + right_axis_samples) : left_y_range; QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing, true); painter.setFont(QFont(QStringLiteral("Sans Serif"), std::clamp(settings.font_size, 6, 16))); - QRectF rect = plotRect(settings, y_range, painter.fontMetrics()); + QRectF rect = plotRect( + settings, + left_y_range, + right_y_range, + has_right_axis, + painter.fontMetrics()); if (settings.x_axis_mode == XAxisMode::Field && - settings.xy_axis_scale_mode == XYAxisScaleMode::Equal) + settings.xy_axis_scale_mode == XYAxisScaleMode::Equal && + !has_right_axis) { - applyEqualXYScale(rect, x_range, y_range); - rect = plotRect(settings, y_range, painter.fontMetrics()); + applyEqualXYScale(rect, x_range, left_y_range); + rect = plotRect( + settings, + left_y_range, + right_y_range, + has_right_axis, + painter.fontMetrics()); } - drawGrid(painter, rect, x_range, y_range, settings); - drawReferences(painter, rect, y_range, references, settings); + drawGrid(painter, rect, x_range, left_y_range, right_y_range, has_right_axis, settings); + drawReferences(painter, rect, left_y_range, references, settings); for (const RenderableSeries & item : series) { - drawSeries(painter, rect, x_range, y_range, item); + const PlotRange & series_y_range = + item.axis == SeriesAxis::Right ? right_y_range : left_y_range; + drawSeries(painter, rect, x_range, series_y_range, item); } drawLegend(painter, rect, series, x_range, settings); return image; diff --git a/test/plot_2d_display_test_helpers.hpp b/test/plot_2d_display_test_helpers.hpp index 4e781dc..b538f11 100644 --- a/test/plot_2d_display_test_helpers.hpp +++ b/test/plot_2d_display_test_helpers.hpp @@ -85,6 +85,11 @@ class Plot2DDisplayTestAccessor return display.y_axis_root_property_; } + static rviz_common::properties::Property * rightYAxisRoot(Plot2DDisplay & display) + { + return display.right_y_axis_root_property_; + } + static rviz_common::properties::Property * xAxisRoot(Plot2DDisplay & display) { return display.x_axis_root_property_; diff --git a/test/test_plot_2d_config.cpp b/test/test_plot_2d_config.cpp index 729578d..bb9deab 100644 --- a/test/test_plot_2d_config.cpp +++ b/test/test_plot_2d_config.cpp @@ -19,6 +19,7 @@ using rviz_2d_plot_plugin::PlotStyle; using rviz_2d_plot_plugin::Plot2DConfig; using rviz_2d_plot_plugin::QoSDurability; using rviz_2d_plot_plugin::QoSReliability; +using rviz_2d_plot_plugin::SeriesAxis; using rviz_2d_plot_plugin::TimeSource; using rviz_2d_plot_plugin::VerticalAlignment; using rviz_2d_plot_plugin::XAxisMode; @@ -33,6 +34,7 @@ TEST(Plot2DConfig, DefaultsDescribeOneUsableTimeSeries) ASSERT_EQ(config.series.size(), 1u); EXPECT_TRUE(config.series.front().enabled); + EXPECT_EQ(config.series.front().axis, SeriesAxis::Left); EXPECT_EQ(config.series.front().topic, ""); EXPECT_EQ(config.series.front().x_field, ""); EXPECT_EQ(config.series.front().y_field, ""); @@ -53,6 +55,10 @@ TEST(Plot2DConfig, DefaultsDescribeOneUsableTimeSeries) EXPECT_DOUBLE_EQ(config.y_axis.fixed_min, -1.0); EXPECT_DOUBLE_EQ(config.y_axis.fixed_max, 1.0); EXPECT_DOUBLE_EQ(config.y_axis.padding_fraction, 0.08); + EXPECT_EQ(config.y_axis_right.scale_mode, AxisScaleMode::Auto); + EXPECT_DOUBLE_EQ(config.y_axis_right.fixed_min, -1.0); + EXPECT_DOUBLE_EQ(config.y_axis_right.fixed_max, 1.0); + EXPECT_DOUBLE_EQ(config.y_axis_right.padding_fraction, 0.08); EXPECT_EQ(config.x_axis.mode, XAxisMode::Time); EXPECT_EQ(config.x_axis.scale_mode, AxisScaleMode::Auto); @@ -108,6 +114,18 @@ TEST(Plot2DConfig, RepairsInvalidFixedXAxisRange) EXPECT_LT(config.x_axis.fixed_min, config.x_axis.fixed_max); } +TEST(Plot2DConfig, RepairsInvalidFixedRightYAxisRange) +{ + Plot2DConfig config; + config.y_axis_right.scale_mode = AxisScaleMode::Fixed; + config.y_axis_right.fixed_min = 8.0; + config.y_axis_right.fixed_max = 8.0; + + config.repair(); + + EXPECT_LT(config.y_axis_right.fixed_min, config.y_axis_right.fixed_max); +} + TEST(Plot2DConfig, RepairsInvalidTimeAndLayoutValues) { Plot2DConfig config; diff --git a/test/test_plot_2d_display_properties.cpp b/test/test_plot_2d_display_properties.cpp index 2a5bb95..b8f286d 100644 --- a/test/test_plot_2d_display_properties.cpp +++ b/test/test_plot_2d_display_properties.cpp @@ -19,6 +19,7 @@ using rviz_2d_plot_plugin::Plot2DDisplay; using rviz_2d_plot_plugin::Plot2DDisplayTestAccessor; using rviz_2d_plot_plugin::QoSDurability; using rviz_2d_plot_plugin::QoSReliability; +using rviz_2d_plot_plugin::SeriesAxis; using rviz_2d_plot_plugin::TimeSource; using rviz_2d_plot_plugin::TopicTypeMap; using rviz_2d_plot_plugin::VerticalAlignment; @@ -47,6 +48,7 @@ TEST(Plot2DDisplay, CreatesMvpPropertyLayout) ASSERT_NE(nullptr, Plot2DDisplayTestAccessor::seriesRoot(display)); ASSERT_NE(nullptr, Plot2DDisplayTestAccessor::timeRoot(display)); ASSERT_NE(nullptr, Plot2DDisplayTestAccessor::yAxisRoot(display)); + ASSERT_NE(nullptr, Plot2DDisplayTestAccessor::rightYAxisRoot(display)); ASSERT_NE(nullptr, Plot2DDisplayTestAccessor::referencesRoot(display)); ASSERT_NE(nullptr, Plot2DDisplayTestAccessor::layoutRoot(display)); EXPECT_EQ(findChild(&display, "Pause Plot"), Plot2DDisplayTestAccessor::pausePlot(display)); @@ -65,6 +67,7 @@ TEST(Plot2DDisplay, CreatesMvpPropertyLayout) EXPECT_NE(nullptr, findChild(series, "X Field")); EXPECT_NE(nullptr, findChild(series, "Y Field")); EXPECT_NE(nullptr, findChild(series, "Field")); + EXPECT_NE(nullptr, findChild(series, "Axis")); EXPECT_FALSE(findChild(series, "Field")->getHidden()); EXPECT_TRUE(findChild(series, "X Field")->getHidden()); EXPECT_TRUE(findChild(series, "Y Field")->getHidden()); @@ -104,6 +107,11 @@ TEST(Plot2DDisplay, CreatesMvpPropertyLayout) EXPECT_NE(nullptr, findChild(y_axis, "Y Min")); EXPECT_NE(nullptr, findChild(y_axis, "Y Max")); + auto * right_y_axis = Plot2DDisplayTestAccessor::rightYAxisRoot(display); + EXPECT_NE(nullptr, findChild(right_y_axis, "Auto Scale")); + EXPECT_NE(nullptr, findChild(right_y_axis, "Y Min")); + EXPECT_NE(nullptr, findChild(right_y_axis, "Y Max")); + auto * grid = Plot2DDisplayTestAccessor::gridRoot(display); ASSERT_NE(nullptr, grid); EXPECT_NE(nullptr, findChild(grid, "Major Grid")); @@ -127,6 +135,14 @@ TEST(Plot2DDisplay, CreatesMvpPropertyLayout) EXPECT_NE(nullptr, findChild(legend, "Position")); EXPECT_NE(nullptr, findChild(legend, "X Offset")); EXPECT_NE(nullptr, findChild(legend, "Y Offset")); + auto * right_legend = findChild(legend, "Right Legend"); + ASSERT_NE(nullptr, right_legend); + EXPECT_NE(nullptr, findChild(right_legend, "Enabled")); + EXPECT_NE(nullptr, findChild(right_legend, "Merge With Left")); + EXPECT_NE(nullptr, findChild(right_legend, "Show Values")); + EXPECT_NE(nullptr, findChild(right_legend, "Position")); + EXPECT_NE(nullptr, findChild(right_legend, "X Offset")); + EXPECT_NE(nullptr, findChild(right_legend, "Y Offset")); auto * layout = Plot2DDisplayTestAccessor::layoutRoot(display); EXPECT_NE(nullptr, findChild(layout, "Width")); @@ -261,6 +277,7 @@ TEST(Plot2DDisplay, BooleanPropertiesUseCheckboxEditing) findChild(series, "Delete"), findChild(Plot2DDisplayTestAccessor::xAxisRoot(display), "Auto Scale"), findChild(Plot2DDisplayTestAccessor::yAxisRoot(display), "Auto Scale"), + findChild(Plot2DDisplayTestAccessor::rightYAxisRoot(display), "Auto Scale"), findChild(Plot2DDisplayTestAccessor::gridRoot(display), "Major Grid"), findChild(Plot2DDisplayTestAccessor::gridRoot(display), "Minor Grid"), }; @@ -287,6 +304,7 @@ TEST(Plot2DDisplay, BuildsPlotConfigFromProperties) findChild(series, "X Field")->setValue("pose/pose/position/x"); findChild(series, "Y Field")->setValue("pose/pose/position/y"); findChild(series, "Field")->setValue("pose/pose/position/y"); + findChild(series, "Axis")->setValue("Right"); findChild(series, "Label")->setValue("Odom Position"); findChild(series, "Unit")->setValue("m"); findChild(series, "Color")->setValue(QColor(255, 80, 20)); @@ -315,6 +333,9 @@ TEST(Plot2DDisplay, BuildsPlotConfigFromProperties) findChild(Plot2DDisplayTestAccessor::yAxisRoot(display), "Auto Scale")->setValue(false); findChild(Plot2DDisplayTestAccessor::yAxisRoot(display), "Y Min")->setValue(-2.0); findChild(Plot2DDisplayTestAccessor::yAxisRoot(display), "Y Max")->setValue(2.0); + findChild(Plot2DDisplayTestAccessor::rightYAxisRoot(display), "Auto Scale")->setValue(false); + findChild(Plot2DDisplayTestAccessor::rightYAxisRoot(display), "Y Min")->setValue(-5.0); + findChild(Plot2DDisplayTestAccessor::rightYAxisRoot(display), "Y Max")->setValue(5.0); findChild(Plot2DDisplayTestAccessor::referencesRoot(display), "Reference Count")->setValue(1); auto * reference = findChild(Plot2DDisplayTestAccessor::referencesRoot(display), "Reference 1"); @@ -344,6 +365,7 @@ TEST(Plot2DDisplay, BuildsPlotConfigFromProperties) EXPECT_EQ(config.series[0].x_field, "pose/pose/position/x"); EXPECT_EQ(config.series[0].y_field, "pose/pose/position/y"); EXPECT_EQ(config.series[0].field, "pose/pose/position/y"); + EXPECT_EQ(config.series[0].axis, SeriesAxis::Right); EXPECT_EQ(config.series[0].label, "Odom Position"); EXPECT_EQ(config.series[0].unit, "m"); EXPECT_EQ(config.series[0].color.red, 255); @@ -371,6 +393,9 @@ TEST(Plot2DDisplay, BuildsPlotConfigFromProperties) EXPECT_EQ(config.y_axis.scale_mode, AxisScaleMode::Fixed); EXPECT_EQ(config.y_axis.fixed_min, -2.0); EXPECT_EQ(config.y_axis.fixed_max, 2.0); + EXPECT_EQ(config.y_axis_right.scale_mode, AxisScaleMode::Fixed); + EXPECT_EQ(config.y_axis_right.fixed_min, -5.0); + EXPECT_EQ(config.y_axis_right.fixed_max, 5.0); ASSERT_EQ(config.references.size(), 1U); EXPECT_TRUE(config.references[0].enabled); EXPECT_DOUBLE_EQ(config.references[0].value, 0.5); @@ -479,12 +504,20 @@ TEST(Plot2DDisplay, MapsLegendPropertiesToRenderSettings) Plot2DDisplay display; auto * legend = Plot2DDisplayTestAccessor::legendRoot(display); ASSERT_NE(nullptr, legend); + auto * right_legend = findChild(legend, "Right Legend"); + ASSERT_NE(nullptr, right_legend); findChild(legend, "Enabled")->setValue(false); findChild(legend, "Show Values")->setValue(false); findChild(legend, "Field Name Only")->setValue(true); findChild(legend, "Position")->setValue("Bottom Right"); findChild(legend, "X Offset")->setValue(12); findChild(legend, "Y Offset")->setValue(8); + findChild(right_legend, "Enabled")->setValue(false); + findChild(right_legend, "Merge With Left")->setValue(true); + findChild(right_legend, "Show Values")->setValue(false); + findChild(right_legend, "Position")->setValue("Bottom Left"); + findChild(right_legend, "X Offset")->setValue(13); + findChild(right_legend, "Y Offset")->setValue(9); const auto settings = Plot2DDisplayTestAccessor::renderSettingsFromProperties(display); EXPECT_FALSE(settings.show_legend); @@ -493,6 +526,12 @@ TEST(Plot2DDisplay, MapsLegendPropertiesToRenderSettings) EXPECT_EQ(settings.legend_position, LegendPosition::BottomRight); EXPECT_EQ(settings.legend_x_offset, 12); EXPECT_EQ(settings.legend_y_offset, 8); + EXPECT_FALSE(settings.show_right_legend); + EXPECT_TRUE(settings.merge_right_legend_with_left); + EXPECT_FALSE(settings.show_right_latest_values); + EXPECT_EQ(settings.right_legend_position, LegendPosition::BottomLeft); + EXPECT_EQ(settings.right_legend_x_offset, 13); + EXPECT_EQ(settings.right_legend_y_offset, 9); } TEST(Plot2DDisplay, MapsGridPropertiesToRenderSettings) @@ -515,6 +554,22 @@ TEST(Plot2DDisplay, MapsGridPropertiesToRenderSettings) EXPECT_EQ(settings.minor_grid_divisions, 2); } +TEST(Plot2DDisplay, MapsRightYAxisPropertiesToRenderSettings) +{ + ensureQtApplication(); + Plot2DDisplay display; + auto * right_y_axis = Plot2DDisplayTestAccessor::rightYAxisRoot(display); + ASSERT_NE(nullptr, right_y_axis); + findChild(right_y_axis, "Auto Scale")->setValue(false); + findChild(right_y_axis, "Y Min")->setValue(-9.0); + findChild(right_y_axis, "Y Max")->setValue(9.0); + + const auto settings = Plot2DDisplayTestAccessor::renderSettingsFromProperties(display); + EXPECT_EQ(settings.right_y_scale_mode, AxisScaleMode::Fixed); + EXPECT_DOUBLE_EQ(settings.fixed_right_y_min, -9.0); + EXPECT_DOUBLE_EQ(settings.fixed_right_y_max, 9.0); +} + TEST(Plot2DDisplay, MapsBackgroundAlphaToRenderSettings) { ensureQtApplication(); diff --git a/test/test_plot_2d_renderer.cpp b/test/test_plot_2d_renderer.cpp index d4f0661..49fb1c0 100644 --- a/test/test_plot_2d_renderer.cpp +++ b/test/test_plot_2d_renderer.cpp @@ -28,6 +28,7 @@ using rviz_2d_plot_plugin::RenderableReference; using rviz_2d_plot_plugin::RenderableSeries; using rviz_2d_plot_plugin::LineStyle; using rviz_2d_plot_plugin::LegendPosition; +using rviz_2d_plot_plugin::SeriesAxis; using rviz_2d_plot_plugin::XAxisMode; namespace @@ -400,6 +401,46 @@ TEST(Plot2DRenderer, IndependentXYAxisScaleKeepsSeparateAxisRanges) EXPECT_GT(bounds.width(), bounds.height() + 80); } +TEST(Plot2DRenderer, RightAxisSeriesUsesIndependentYScale) +{ + ensureQtApplication(); + Plot2DRenderer renderer; + PlotRenderSettings settings; + settings.width = 360; + settings.height = 220; + settings.now = 10.0; + settings.window_seconds = 5.0; + settings.y_scale_mode = rviz_2d_plot_plugin::AxisScaleMode::Fixed; + settings.fixed_y_min = -1.0; + settings.fixed_y_max = 1.0; + settings.right_y_scale_mode = rviz_2d_plot_plugin::AxisScaleMode::Fixed; + settings.fixed_right_y_min = -10.0; + settings.fixed_right_y_max = 10.0; + settings.show_legend = false; + + RenderableSeries left_series; + left_series.label = "Left"; + left_series.color = QColor(250, 40, 40); + left_series.axis = SeriesAxis::Left; + left_series.plot_style = PlotStyle::Points; + left_series.samples = std::vector{{10.0, 0.8}}; + + RenderableSeries right_series; + right_series.label = "Right"; + right_series.color = QColor(40, 200, 255); + right_series.axis = SeriesAxis::Right; + right_series.plot_style = PlotStyle::Points; + right_series.samples = std::vector{{10.0, 0.8}}; + + const QImage image = renderer.render(settings, {left_series, right_series}); + const QRect left_bounds = coloredPixelBounds(image, left_series.color); + const QRect right_bounds = coloredPixelBounds(image, right_series.color); + + ASSERT_FALSE(left_bounds.isNull()); + ASSERT_FALSE(right_bounds.isNull()); + EXPECT_GT(right_bounds.center().y() - left_bounds.center().y(), 20); +} + TEST(Plot2DRenderer, AppliesConfiguredLineWidth) { ensureQtApplication(); @@ -714,6 +755,91 @@ TEST(Plot2DRenderer, OmitsLatestValuesFromLegendWhenConfigured) EXPECT_GT(countDifferentPixels(with_values, without_values), 0); } +TEST(Plot2DRenderer, RightLegendLatestValuesAreIndependent) +{ + ensureQtApplication(); + Plot2DRenderer renderer; + PlotRenderSettings settings; + settings.width = 320; + settings.height = 160; + settings.now = 10.0; + settings.window_seconds = 5.0; + settings.y_scale_mode = rviz_2d_plot_plugin::AxisScaleMode::Fixed; + settings.fixed_y_min = -1.0; + settings.fixed_y_max = 1.0; + settings.right_y_scale_mode = rviz_2d_plot_plugin::AxisScaleMode::Fixed; + settings.fixed_right_y_min = -1.0; + settings.fixed_right_y_max = 1.0; + settings.show_latest_values = false; + settings.show_right_legend = true; + settings.show_right_latest_values = true; + + RenderableSeries left; + left.label = "Left"; + left.color = QColor(250, 40, 40); + left.axis = SeriesAxis::Left; + left.samples = std::vector{{10.0, 0.25}}; + + RenderableSeries right; + right.label = "Right"; + right.color = QColor(40, 120, 255); + right.axis = SeriesAxis::Right; + right.samples = std::vector{{10.0, -0.5}}; + + const QImage with_right_values = renderer.render(settings, {left, right}); + settings.show_right_latest_values = false; + const QImage without_right_values = renderer.render(settings, {left, right}); + + EXPECT_GT(countDifferentPixels(with_right_values, without_right_values), 0); +} + +TEST(Plot2DRenderer, RightLegendCanMergeWithLeftLegend) +{ + ensureQtApplication(); + Plot2DRenderer renderer; + PlotRenderSettings settings; + settings.width = 320; + settings.height = 160; + settings.now = 10.0; + settings.window_seconds = 5.0; + settings.y_scale_mode = rviz_2d_plot_plugin::AxisScaleMode::Fixed; + settings.fixed_y_min = -1.0; + settings.fixed_y_max = 1.0; + settings.right_y_scale_mode = rviz_2d_plot_plugin::AxisScaleMode::Fixed; + settings.fixed_right_y_min = -1.0; + settings.fixed_right_y_max = 1.0; + settings.legend_position = LegendPosition::TopLeft; + settings.right_legend_position = LegendPosition::BottomRight; + settings.show_latest_values = false; + settings.show_right_legend = true; + settings.show_right_latest_values = false; + + RenderableSeries left; + left.label = "Left"; + left.color = QColor(250, 40, 40); + left.axis = SeriesAxis::Left; + + RenderableSeries right; + right.label = "Right"; + right.color = QColor(40, 120, 255); + right.axis = SeriesAxis::Right; + + settings.merge_right_legend_with_left = false; + const QImage separate = renderer.render(settings, {left, right}); + settings.merge_right_legend_with_left = true; + const QImage merged = renderer.render(settings, {left, right}); + + EXPECT_GT( + countPixelsCloseToInRect(separate, right.color, QRect(210, 116, 95, 34)), + 0); + EXPECT_EQ( + countPixelsCloseToInRect(merged, right.color, QRect(210, 116, 95, 34)), + 0); + EXPECT_GT( + countPixelsCloseToInRect(merged, right.color, QRect(38, 14, 130, 50)), + 0); +} + TEST(Plot2DRenderer, AppendsLegendUnitOnlyWhenLatestValueIsShown) { ensureQtApplication(); From 5f071dc06099b7e94a8ed1d6d2d124c88ffdca10 Mon Sep 17 00:00:00 2001 From: Vsevolod Hulchuk Date: Wed, 29 Jul 2026 18:01:09 -0400 Subject: [PATCH 3/3] feat: implement dual display surface options for 2D plots --- .../rviz_2d_plot_plugin/plot_2d_config.hpp | 7 + .../rviz_2d_plot_plugin/plot_2d_display.hpp | 12 ++ src/plot_2d_display.cpp | 177 +++++++++++++++++- src/plot_2d_display_options.cpp | 28 +++ src/plot_2d_display_options.hpp | 3 + test/test_plot_2d_display_properties.cpp | 22 +++ test/test_plot_2d_display_rendering.cpp | 27 +++ 7 files changed, 266 insertions(+), 10 deletions(-) diff --git a/include/rviz_2d_plot_plugin/plot_2d_config.hpp b/include/rviz_2d_plot_plugin/plot_2d_config.hpp index 253deda..dc8482a 100644 --- a/include/rviz_2d_plot_plugin/plot_2d_config.hpp +++ b/include/rviz_2d_plot_plugin/plot_2d_config.hpp @@ -49,6 +49,12 @@ enum class PlotMode XY, }; +enum class DisplaySurface +{ + Overlay, + Panel, +}; + enum class XYHistoryMode { RollingTimeWindow, @@ -215,6 +221,7 @@ struct Plot2DConfig std::vector series{SeriesConfig{}}; std::vector references; PlotMode plot_mode{PlotMode::TimeSeries}; + DisplaySurface display_surface{DisplaySurface::Panel}; XAxisConfig x_axis; AxisConfig y_axis; AxisConfig y_axis_right; diff --git a/include/rviz_2d_plot_plugin/plot_2d_display.hpp b/include/rviz_2d_plot_plugin/plot_2d_display.hpp index 68a876b..d953bd6 100644 --- a/include/rviz_2d_plot_plugin/plot_2d_display.hpp +++ b/include/rviz_2d_plot_plugin/plot_2d_display.hpp @@ -22,6 +22,8 @@ #include "rviz_2d_plot_plugin/plot_2d_renderer.hpp" #include "rviz_2d_plot_plugin/topic_field_introspection.hpp" +class QSize; + namespace rviz_common { class Config; @@ -43,6 +45,7 @@ namespace rviz_2d_plot_plugin { class OverlayBackend; +class PlotImagePanelWidget; class Plot2DDisplayTestAccessor; class Plot2DSubscriptionManager; @@ -68,6 +71,7 @@ class Plot2DDisplay : public rviz_common::Display private Q_SLOTS: void onConfigPropertyChanged(); void onRenderPropertyChanged(); + void onDisplaySurfaceChanged(); void onSeriesAppearancePropertyChanged(); void onReferencePropertyChanged(); void onPlotModeChanged(); @@ -171,6 +175,7 @@ private Q_SLOTS: RenderSnapshot renderSnapshot_() const; PlotRenderSettings renderSettingsFromProperties_() const; PlotRenderSettings renderSettingsFromConfig_(const Plot2DConfig & config) const; + PlotRenderSettings renderSettingsForSurface_(const Plot2DConfig & config) const; std::vector renderableSeries_() const; std::vector renderableSeriesFromSnapshot_( const RenderSnapshot & snapshot) const; @@ -178,8 +183,12 @@ private Q_SLOTS: std::vector renderableReferencesFromConfig_( const Plot2DConfig & config) const; void initializeOverlayBackend_(); + void initializePanelWidget_(); + void synchronizePresentationMode_(const Plot2DConfig & config); + void updatePresentationPropertyVisibility_(const Plot2DConfig & config); void updateOverlayGeometry_(); void updateOverlayGeometry_(const Plot2DConfig & config); + QSize panelRenderSize_() const; void renderOverlay_(bool request_rviz_render = true); void unsubscribe_(); bool shouldRetrySubscriptions_() const; @@ -191,6 +200,7 @@ private Q_SLOTS: rviz_common::properties::BoolProperty * pause_plot_property_{nullptr}; rviz_common::properties::BoolProperty * clear_history_property_{nullptr}; + rviz_common::properties::EnumProperty * display_surface_property_{nullptr}; rviz_common::properties::EnumProperty * plot_mode_property_{nullptr}; rviz_common::properties::Property * series_root_property_{nullptr}; rviz_common::properties::IntProperty * series_count_property_{nullptr}; @@ -264,6 +274,8 @@ private Q_SLOTS: rviz_common::properties::IntProperty * font_size_property_{nullptr}; std::unique_ptr overlay_backend_; + PlotImagePanelWidget * panel_widget_{nullptr}; + bool panel_widget_registered_{false}; std::function(std::string)> overlay_backend_factory_; rclcpp::Node::SharedPtr node_; RosGraphOps ros_graph_ops_; diff --git a/src/plot_2d_display.cpp b/src/plot_2d_display.cpp index 929fdbd..1ebc639 100644 --- a/src/plot_2d_display.cpp +++ b/src/plot_2d_display.cpp @@ -8,10 +8,14 @@ #include #include +#include #include +#include #include #include #include +#include +#include #include #include @@ -42,6 +46,40 @@ namespace rviz_2d_plot_plugin { + +class PlotImagePanelWidget final : public QWidget +{ +public: + explicit PlotImagePanelWidget(QWidget * parent = nullptr) + : QWidget(parent), image_label_(new QLabel(this)) + { + auto * layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(image_label_); + image_label_->setAlignment(Qt::AlignCenter); + image_label_->setScaledContents(false); + setMinimumSize(120, 80); + resize(360, 220); + } + + void setImage(const QImage & image) + { + image_label_->setPixmap(QPixmap::fromImage(image)); + } + + QSize renderSize() const + { + const QSize content = image_label_->contentsRect().size(); + if (content.width() > 0 && content.height() > 0) { + return content; + } + return size(); + } + +private: + QLabel * image_label_; +}; + namespace { @@ -96,6 +134,13 @@ Plot2DDisplay::Plot2DDisplay() "Clear History", false, "Clear stored samples for this plot.", this, SLOT(onClearHistoryChanged()), this); + display_surface_property_ = new rviz_common::properties::EnumProperty( + "Display Surface", + QString::fromStdString(displaySurfaceName(DisplaySurface::Panel)), + "Choose whether the plot is shown as a 3D viewport overlay or in a dockable panel.", + this, SLOT(onDisplaySurfaceChanged()), this); + addDisplaySurfaceOptions(display_surface_property_); + plot_mode_property_ = new rviz_common::properties::EnumProperty( "Plot Mode", QString::fromStdString(plotModeName(PlotMode::TimeSeries)), "Choose time-series or same-topic XY plotting.", @@ -350,9 +395,16 @@ Plot2DDisplay::Plot2DDisplay() "Font Size", 8, "Axis, legend, and reference label font size in points.", style_root_property_, SLOT(onRenderPropertyChanged()), this, 6, 16); updateModePropertyVisibility_(); + updatePresentationPropertyVisibility_(configFromProperties_()); } -Plot2DDisplay::~Plot2DDisplay() = default; +Plot2DDisplay::~Plot2DDisplay() +{ + if (panel_widget_registered_) { + setAssociatedWidget(nullptr); + panel_widget_registered_ = false; + } +} void Plot2DDisplay::load(const rviz_common::Config & config) { @@ -369,6 +421,7 @@ void Plot2DDisplay::load(const rviz_common::Config & config) rviz_common::Display::load(config); updateModePropertyVisibility_(); + updatePresentationPropertyVisibility_(configFromProperties_()); updateSeriesPropertySummaries_(); updateReferencePropertySummaries_(); } @@ -404,13 +457,17 @@ void Plot2DDisplay::onInitialize() } initializeOverlayBackend_(); + initializePanelWidget_(); + synchronizePresentationMode_(configFromProperties_()); resolveAndSubscribe_(); } void Plot2DDisplay::onEnable() { + const Plot2DConfig config = configFromProperties_(); resolveAndSubscribe_(); - if (overlay_backend_) { + synchronizePresentationMode_(config); + if (config.display_surface == DisplaySurface::Overlay && overlay_backend_) { overlay_backend_->setVisible(true); } renderOverlay_(); @@ -422,6 +479,9 @@ void Plot2DDisplay::onDisable() if (overlay_backend_) { overlay_backend_->setVisible(false); } + if (panel_widget_registered_ && getAssociatedWidget()) { + getAssociatedWidget()->hide(); + } } void Plot2DDisplay::update(const float wall_dt, const float ros_dt) @@ -466,6 +526,14 @@ void Plot2DDisplay::onRenderPropertyChanged() renderOverlay_(); } +void Plot2DDisplay::onDisplaySurfaceChanged() +{ + const Plot2DConfig config = configFromProperties_(); + synchronizePresentationMode_(config); + updatePresentationPropertyVisibility_(config); + renderOverlay_(); +} + void Plot2DDisplay::onSeriesAppearancePropertyChanged() { updateSeriesPropertySummaries_(); @@ -770,6 +838,9 @@ Plot2DConfig Plot2DDisplay::configFromProperties_() const config.references = referenceConfigFromProperties_(); config.plot_mode = plot_mode_property_ ? plotModeFromName(plot_mode_property_->getStdString()) : PlotMode::TimeSeries; + config.display_surface = display_surface_property_ ? + displaySurfaceFromName(display_surface_property_->getStdString()) : + DisplaySurface::Overlay; config.time.window_seconds = window_seconds_property_->getFloat(); config.time.refresh_rate_hz = refresh_rate_property_->getFloat(); @@ -1225,6 +1296,12 @@ void Plot2DDisplay::updateModePropertyVisibility_() series.y_field->setHidden(!xy_mode); } } + + Plot2DConfig config; + config.display_surface = display_surface_property_ ? + displaySurfaceFromName(display_surface_property_->getStdString()) : + DisplaySurface::Overlay; + updatePresentationPropertyVisibility_(config); } void Plot2DDisplay::updateSeriesPropertySummaries_() @@ -1442,7 +1519,7 @@ Plot2DDisplay::RenderSnapshot Plot2DDisplay::renderSnapshot_() const PlotRenderSettings Plot2DDisplay::renderSettingsFromProperties_() const { - return renderSettingsFromConfig_(configFromProperties_()); + return renderSettingsForSurface_(configFromProperties_()); } PlotRenderSettings Plot2DDisplay::renderSettingsFromConfig_(const Plot2DConfig & config) const @@ -1512,6 +1589,19 @@ PlotRenderSettings Plot2DDisplay::renderSettingsFromConfig_(const Plot2DConfig & return settings; } +PlotRenderSettings Plot2DDisplay::renderSettingsForSurface_(const Plot2DConfig & config) const +{ + PlotRenderSettings settings = renderSettingsFromConfig_(config); + if (config.display_surface != DisplaySurface::Panel) { + return settings; + } + + const QSize panel_size = panelRenderSize_(); + settings.width = std::max(panel_size.width(), 120); + settings.height = std::max(panel_size.height(), 80); + return settings; +} + std::vector Plot2DDisplay::renderableSeries_() const { return renderableSeriesFromSnapshot_(renderSnapshot_()); @@ -1605,6 +1695,54 @@ void Plot2DDisplay::initializeOverlayBackend_() overlay_backend_->setVisible(false); } +void Plot2DDisplay::initializePanelWidget_() +{ + if (panel_widget_) { + return; + } + + panel_widget_ = new PlotImagePanelWidget(); + panel_widget_->setWindowTitle(getName()); +} + +void Plot2DDisplay::synchronizePresentationMode_(const Plot2DConfig & config) +{ + const bool panel_mode = config.display_surface == DisplaySurface::Panel; + if (panel_mode) { + initializePanelWidget_(); + if (panel_widget_) { + panel_widget_->setWindowTitle(getName()); + } + if (overlay_backend_) { + overlay_backend_->setVisible(false); + } + if (isEnabled() && panel_widget_ && !panel_widget_registered_) { + setAssociatedWidget(panel_widget_); + panel_widget_registered_ = true; + } + if (isEnabled() && panel_widget_registered_ && getAssociatedWidget()) { + getAssociatedWidget()->show(); + } + return; + } + + if (panel_widget_registered_ && getAssociatedWidget()) { + getAssociatedWidget()->hide(); + } + if (overlay_backend_ && isEnabled()) { + overlay_backend_->setVisible(isEnabled()); + } +} + +void Plot2DDisplay::updatePresentationPropertyVisibility_(const Plot2DConfig & config) +{ + if (!layout_root_property_) { + return; + } + const bool panel_mode = config.display_surface == DisplaySurface::Panel; + layout_root_property_->setHidden(panel_mode); +} + void Plot2DDisplay::updateOverlayGeometry_() { updateOverlayGeometry_(configFromProperties_()); @@ -1636,13 +1774,38 @@ void Plot2DDisplay::updateOverlayGeometry_(const Plot2DConfig & config) } } +QSize Plot2DDisplay::panelRenderSize_() const +{ + if (panel_widget_) { + return panel_widget_->renderSize(); + } + return QSize( + width_property_ ? width_property_->getInt() : 360, + height_property_ ? height_property_->getInt() : 220); +} + void Plot2DDisplay::renderOverlay_(const bool request_rviz_render) { + const RenderSnapshot snapshot = renderSnapshot_(); + synchronizePresentationMode_(snapshot.config); + + const PlotRenderSettings settings = renderSettingsForSurface_(snapshot.config); + const QImage rendered = renderer_.render( + settings, + renderableSeriesFromSnapshot_(snapshot), + renderableReferencesFromConfig_(snapshot.config)); + + if (snapshot.config.display_surface == DisplaySurface::Panel) { + if (panel_widget_ && panel_widget_registered_) { + panel_widget_->setImage(rendered); + } + return; + } + if (!overlay_backend_) { return; } - const RenderSnapshot snapshot = renderSnapshot_(); updateOverlayGeometry_(snapshot.config); if (isEnabled()) { overlay_backend_->setVisible(true); @@ -1651,12 +1814,6 @@ void Plot2DDisplay::renderOverlay_(const bool request_rviz_render) return; } - const PlotRenderSettings settings = renderSettingsFromConfig_(snapshot.config); - const QImage rendered = renderer_.render( - settings, - renderableSeriesFromSnapshot_(snapshot), - renderableReferencesFromConfig_(snapshot.config)); - const OverlayBackendResult image_result = overlay_backend_->updateImage(rendered); if (!image_result.ok()) { setStatus( diff --git a/src/plot_2d_display_options.cpp b/src/plot_2d_display_options.cpp index 3c2fb04..18659f3 100644 --- a/src/plot_2d_display_options.cpp +++ b/src/plot_2d_display_options.cpp @@ -235,6 +235,34 @@ void addPlotModeOptions(rviz_common::properties::EnumProperty * property) property->addOptionStd(plotModeName(PlotMode::XY)); } +std::string displaySurfaceName(const DisplaySurface surface) +{ + switch (surface) { + case DisplaySurface::Overlay: + return "3D Overlay"; + case DisplaySurface::Panel: + return "Dock Panel"; + } + return "3D Overlay"; +} + +DisplaySurface displaySurfaceFromName(const std::string & name) +{ + if (name == "Dock Panel") { + return DisplaySurface::Panel; + } + return DisplaySurface::Overlay; +} + +void addDisplaySurfaceOptions(rviz_common::properties::EnumProperty * property) +{ + if (!property) { + return; + } + property->addOptionStd(displaySurfaceName(DisplaySurface::Overlay)); + property->addOptionStd(displaySurfaceName(DisplaySurface::Panel)); +} + std::string seriesDefaultLabel(const SeriesConfig & series, const PlotMode plot_mode) { if (series.topic.empty()) { diff --git a/src/plot_2d_display_options.hpp b/src/plot_2d_display_options.hpp index 347c973..c869b7b 100644 --- a/src/plot_2d_display_options.hpp +++ b/src/plot_2d_display_options.hpp @@ -54,6 +54,9 @@ void addQoSDurabilityOptions(rviz_common::properties::EnumProperty * property); std::string plotModeName(PlotMode mode); PlotMode plotModeFromName(const std::string & name); void addPlotModeOptions(rviz_common::properties::EnumProperty * property); +std::string displaySurfaceName(DisplaySurface surface); +DisplaySurface displaySurfaceFromName(const std::string & name); +void addDisplaySurfaceOptions(rviz_common::properties::EnumProperty * property); std::string seriesDefaultLabel(const SeriesConfig & series, PlotMode plot_mode); std::string seriesAxisName(SeriesAxis axis); SeriesAxis seriesAxisFromName(const std::string & name); diff --git a/test/test_plot_2d_display_properties.cpp b/test/test_plot_2d_display_properties.cpp index b8f286d..16eff20 100644 --- a/test/test_plot_2d_display_properties.cpp +++ b/test/test_plot_2d_display_properties.cpp @@ -10,6 +10,7 @@ namespace { using rviz_2d_plot_plugin::AxisScaleMode; +using rviz_2d_plot_plugin::DisplaySurface; using rviz_2d_plot_plugin::HorizontalAlignment; using rviz_2d_plot_plugin::LegendPosition; using rviz_2d_plot_plugin::Plot2DConfig; @@ -53,6 +54,7 @@ TEST(Plot2DDisplay, CreatesMvpPropertyLayout) ASSERT_NE(nullptr, Plot2DDisplayTestAccessor::layoutRoot(display)); EXPECT_EQ(findChild(&display, "Pause Plot"), Plot2DDisplayTestAccessor::pausePlot(display)); EXPECT_EQ(findChild(&display, "Clear History"), Plot2DDisplayTestAccessor::clearHistory(display)); + EXPECT_NE(nullptr, findChild(&display, "Display Surface")); EXPECT_NE(nullptr, findChild(&display, "Plot Mode")); auto * series = findChild(Plot2DDisplayTestAccessor::seriesRoot(display), "Series 1"); @@ -194,6 +196,24 @@ TEST(Plot2DDisplay, PlotModeSwitchesBetweenTimeAndXYSeriesFields) EXPECT_FALSE(xy_history_mode->getHidden()); } +TEST(Plot2DDisplay, DisplaySurfaceToggleHidesOverlayLayoutProperties) +{ + ensureQtApplication(); + Plot2DDisplay display; + auto * layout = Plot2DDisplayTestAccessor::layoutRoot(display); + ASSERT_NE(nullptr, layout); + auto * display_surface = findChild(&display, "Display Surface"); + ASSERT_NE(nullptr, display_surface); + + EXPECT_TRUE(layout->getHidden()); + + display_surface->setValue("3D Overlay"); + EXPECT_FALSE(layout->getHidden()); + + display_surface->setValue("Dock Panel"); + EXPECT_TRUE(layout->getHidden()); +} + TEST(Plot2DDisplay, SeriesRootShowsConfiguredSourceForPlotMode) { ensureQtApplication(); @@ -356,6 +376,7 @@ TEST(Plot2DDisplay, BuildsPlotConfigFromProperties) "Center"); findChild(Plot2DDisplayTestAccessor::layoutRoot(display), "Vertical Alignment")->setValue( "Bottom"); + findChild(&display, "Display Surface")->setValue("Dock Panel"); const Plot2DConfig config = Plot2DDisplayTestAccessor::configFromProperties(display); @@ -385,6 +406,7 @@ TEST(Plot2DDisplay, BuildsPlotConfigFromProperties) EXPECT_EQ(config.qos.durability, QoSDurability::TransientLocal); EXPECT_EQ(config.qos.depth, 42); EXPECT_EQ(config.plot_mode, PlotMode::XY); + EXPECT_EQ(config.display_surface, DisplaySurface::Panel); EXPECT_EQ(config.x_axis.mode, XAxisMode::Field); EXPECT_EQ(config.x_axis.scale_mode, AxisScaleMode::Fixed); EXPECT_EQ(config.x_axis.axis_scale_mode, XYAxisScaleMode::Equal); diff --git a/test/test_plot_2d_display_rendering.cpp b/test/test_plot_2d_display_rendering.cpp index 5df038a..e1fe784 100644 --- a/test/test_plot_2d_display_rendering.cpp +++ b/test/test_plot_2d_display_rendering.cpp @@ -49,6 +49,7 @@ TEST(Plot2DDisplay, InitializesInjectedOverlayBackend) backend = created.get(); return created; }); + findChild(&display, "Display Surface")->setValue("3D Overlay"); Plot2DDisplayTestAccessor::initializeOverlayBackend(display); @@ -99,6 +100,7 @@ TEST(Plot2DDisplay, RenderOverlayUpdatesGeometryBeforeImageUpload) backend = created.get(); return created; }); + findChild(&display, "Display Surface")->setValue("3D Overlay"); findChild(Plot2DDisplayTestAccessor::layoutRoot(display), "Width")->setValue(420); findChild(Plot2DDisplayTestAccessor::layoutRoot(display), "Height")->setValue(180); Plot2DDisplayTestAccessor::initializeOverlayBackend(display); @@ -123,6 +125,31 @@ TEST(Plot2DDisplay, RenderOverlayUpdatesGeometryBeforeImageUpload) EXPECT_EQ(backend->image_sizes[0], std::make_pair(420, 180)); } +TEST(Plot2DDisplay, DockPanelSurfaceSkipsOverlayImageUpload) +{ + ensureQtApplication(); + Plot2DDisplay display; + RecordingOverlayBackend * backend = nullptr; + Plot2DDisplayTestAccessor::setOverlayBackendFactory( + display, + [&backend](std::string) { + auto created = std::make_unique(); + backend = created.get(); + return created; + }); + findChild(&display, "Display Surface")->setValue("Dock Panel"); + Plot2DDisplayTestAccessor::initializeOverlayBackend(display); + ASSERT_NE(nullptr, backend); + backend->events.clear(); + backend->geometries.clear(); + backend->image_sizes.clear(); + + Plot2DDisplayTestAccessor::renderOverlay(display); + + EXPECT_TRUE(backend->geometries.empty()); + EXPECT_TRUE(backend->image_sizes.empty()); +} + TEST(Plot2DDisplay, KeepsConfiguredSeriesRenderableBeforeTopicResolves) { ensureQtApplication();