Repository: ros2/rviz
Pull request #1453: Backported FrameAligned camera
Author: login: georgflick
Created: 2025-05-02T12:46:13Z | Merged: 2025-05-06T20:35:36Z

Backported the Frame Aligned camera from [rViz](https://github.com/ros-visualization/rviz/blob/noetic-devel/src/rviz/default_plugin/view_controllers/frame_view_controller.cpp) to rviz ros2 and also added some tests for it.
![Screenshot from 2025-05-02 14-39-54](https://github.com/user-attachments/assets/eee66a22-2236-4518-926a-2dfb22cf1381)

This solved the request from this issue: https://github.com/ros2/rviz/issues/1320

I tested it on ros2 jazzy, but the changes should also work on rolling.

To test:
- compile and launch rviz2
- select in views the Type: FrameAligned
- Change the axis in the `Points towards` entry
- Enable lock camera and try to move the camera with the mouse


Diff:
diff --git a/README.md b/README.md
index 4367f51ed..3630ad293 100644
--- a/README.md
+++ b/README.md
@@ -17,9 +17,9 @@ For some displays, the [documentation is updated](docs/FEATURES.md).
 | Axes                  | Move Camera   | Orbit                 | Displays        |
 | Camera                | Focus Camera  | XY Orbit              | Help            |
 | DepthCloud            | Measure       | First Person          | Selections      |
-| Effort                | Select        | Third Person Follower | Time            |
-| Fluid                 | 2D Nav Goal   | Top Down Orthographic | Tool Properties |
-| Grid                  | Publish Point |                       | Views           |
+| Effort                | Select        | Frame Aligned         | Time            |
+| Fluid                 | 2D Nav Goal   | Third Person Follower | Tool Properties |
+| Grid                  | Publish Point | Top Down Orthographic | Views           |
 | Grid Cells            | Initial Pose  |
 | Illuminance           | Interact      |
 | Image                 |
diff --git a/rviz_default_plugins/CMakeLists.txt b/rviz_default_plugins/CMakeLists.txt
index 3995d866c..63a189b5f 100644
--- a/rviz_default_plugins/CMakeLists.txt
+++ b/rviz_default_plugins/CMakeLists.txt
@@ -130,6 +130,7 @@ set(rviz_default_plugins_headers_to_moc
   include/rviz_default_plugins/tools/interaction/interaction_tool.hpp
   include/rviz_default_plugins/tools/pose_estimate/initial_pose_tool.hpp
   include/rviz_default_plugins/tools/point/point_tool.hpp
+  include/rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp
   include/rviz_default_plugins/view_controllers/orbit/orbit_view_controller.hpp
   include/rviz_default_plugins/view_controllers/ortho/fixed_orientation_ortho_view_controller.hpp
   include/rviz_default_plugins/view_controllers/xy_orbit/xy_orbit_view_controller.hpp
@@ -228,6 +229,7 @@ set(rviz_default_plugins_source_files
   src/rviz_default_plugins/transformation/tf_wrapper.cpp
   src/rviz_default_plugins/view_controllers/follower/third_person_follower_view_controller.cpp
   src/rviz_default_plugins/view_controllers/fps/fps_view_controller.cpp
+  src/rviz_default_plugins/view_controllers/frame/frame_view_controller.cpp
   src/rviz_default_plugins/view_controllers/orbit/orbit_view_controller.cpp
   src/rviz_default_plugins/view_controllers/ortho/fixed_orientation_ortho_view_controller.cpp
   src/rviz_default_plugins/view_controllers/xy_orbit/xy_orbit_view_controller.cpp
@@ -373,6 +375,20 @@ if(BUILD_TESTING)
     )
   endif()
 
+  ament_add_gmock(frame_view_controller_test
+    test/rviz_default_plugins/view_controllers/frame/frame_view_controller_test.cpp
+    ${TEST_FIXTURE_SOURCES_WITH_MOCK}
+    ${SKIP_DISPLAY_TESTS})
+  if(TARGET frame_view_controller_test)
+    target_include_directories(frame_view_controller_test PRIVATE ${TEST_INCLUDE_DIRS})
+    target_link_libraries(frame_view_controller_test
+      ${TEST_FIXTURE_WITH_MOCK_LIBRARIES}
+      rviz_default_plugins
+      Qt5::Widgets
+      ogre_testing_environment
+    )
+  endif()
+
   ament_add_gmock(frame_info_test
     test/rviz_default_plugins/displays/tf/frame_info_test.cpp
     ${TEST_FIXTURE_SOURCES_WITH_MOCK}
diff --git a/rviz_default_plugins/include/rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp b/rviz_default_plugins/include/rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp
new file mode 100644
index 000000000..e56fa5d0e
--- /dev/null
+++ b/rviz_default_plugins/include/rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp
@@ -0,0 +1,100 @@
+// Copyright (c) 2012, Willow Garage, Inc.
+// Copyright (c) 2018, Bosch Software Innovations GmbH.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//    * Redistributions of source code must retain the above copyright
+//      notice, this list of conditions and the following disclaimer.
+//
+//    * Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+//
+//    * Neither the name of the copyright holder nor the names of its
+//      contributors may be used to endorse or promote products derived from
+//      this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+
+#ifndef RVIZ_DEFAULT_PLUGINS__VIEW_CONTROLLERS__FRAME__FRAME_VIEW_CONTROLLER_HPP_
+#define RVIZ_DEFAULT_PLUGINS__VIEW_CONTROLLERS__FRAME__FRAME_VIEW_CONTROLLER_HPP_
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wkeyword-macro"
+#endif
+
+#include <OgreVector.h>
+#include <OgreQuaternion.h>
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+#include "rviz_default_plugins/view_controllers/fps/fps_view_controller.hpp"
+
+#include "rviz_common/properties/bool_property.hpp"
+#include "rviz_common/properties/enum_property.hpp"
+
+namespace rviz_default_plugins
+{
+namespace view_controllers
+{
+/** @brief A frame-aligned camera. */
+class RVIZ_DEFAULT_PLUGINS_PUBLIC FrameViewController : public FPSViewController
+{
+  Q_OBJECT
+
+public:
+  FrameViewController();
+
+  ~FrameViewController() override = default;
+
+  void onInitialize() override;
+
+  void handleMouseEvent(rviz_common::ViewportMouseEvent & evt) override;
+
+  void reset() override;
+
+  void yaw(float angle);
+
+  void pitch(float angle);
+
+protected:
+  void onTargetFrameChanged(
+    const Ogre::Vector3 & old_reference_position,
+    const Ogre::Quaternion & old_reference_orientation) override;
+
+  rviz_common::properties::EnumProperty * axis_property_;
+  rviz_common::properties::BoolProperty * locked_property_;
+
+  int actualCameraAxisOption(double precision = 0.001) const;
+
+  void setAxisFromCamera();
+
+protected Q_SLOTS:
+  void changedAxis();
+
+private:
+  void rememberAxis(int current);
+
+  int previous_axis_;
+};
+
+}  // namespace view_controllers
+}  // namespace rviz_default_plugins
+
+#endif  // RVIZ_DEFAULT_PLUGINS__VIEW_CONTROLLERS__FRAME__FRAME_VIEW_CONTROLLER_HPP_
diff --git a/rviz_default_plugins/plugins_description.xml b/rviz_default_plugins/plugins_description.xml
index 0b5039577..6b0b581f6 100644
--- a/rviz_default_plugins/plugins_description.xml
+++ b/rviz_default_plugins/plugins_description.xml
@@ -457,7 +457,15 @@
       Control the camera like in a First Person Shooter game: drag left to look left, etc.
     </description>
   </class>
-
+  <class
+    name="rviz_default_plugins/FrameAligned"
+    type="rviz_default_plugins::view_controllers::FrameViewController"
+    base_class_type="rviz_common::ViewController"
+  >
+    <description>
+      Align the camera with a TF frame.
+    </description>
+  </class>
   <class
     name="rviz_default_plugins/Orbit"
     type="rviz_default_plugins::view_controllers::OrbitViewController"
diff --git a/rviz_default_plugins/src/rviz_default_plugins/view_controllers/frame/frame_view_controller.cpp b/rviz_default_plugins/src/rviz_default_plugins/view_controllers/frame/frame_view_controller.cpp
new file mode 100644
index 000000000..20b726f5c
--- /dev/null
+++ b/rviz_default_plugins/src/rviz_default_plugins/view_controllers/frame/frame_view_controller.cpp
@@ -0,0 +1,176 @@
+// Copyright (c) 2012, Willow Garage, Inc.
+// Copyright (c) 2018, Bosch Software Innovations GmbH.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//    * Redistributions of source code must retain the above copyright
+//      notice, this list of conditions and the following disclaimer.
+//
+//    * Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+//
+//    * Neither the name of the copyright holder nor the names of its
+//      contributors may be used to endorse or promote products derived from
+//      this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+
+#include "rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp"
+
+#include <cmath>
+
+#include <OgreCamera.h>
+#include <OgreQuaternion.h>
+#include <OgreSceneNode.h>
+#include <OgreSceneManager.h>
+#include <OgreVector.h>
+#include <OgreViewport.h>
+
+#include "rviz_rendering/geometry.hpp"
+#include "rviz_rendering/objects/shape.hpp"
+
+#include "rviz_common/display_context.hpp"
+#include "rviz_common/viewport_mouse_event.hpp"
+#include "rviz_common/uniform_string_stream.hpp"
+
+namespace rviz_default_plugins
+{
+namespace view_controllers
+{
+static const QString ANY_AXIS("arbitrary");
+
+// helper function to create axis strings from option ID
+inline QString fmtAxis(int i)
+{
+  return QString("%1%2 axis").arg(QChar(i % 2 ? '+' : '-')).arg(QChar('x' + (i - 1) / 2));
+}
+
+static const Ogre::Quaternion ROBOT_TO_CAMERA_ROTATION =
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Y) *
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Z);
+
+static const float PITCH_LIMIT_LOW = -Ogre::Math::HALF_PI + 0.001f;
+static const float PITCH_LIMIT_HIGH = Ogre::Math::HALF_PI - 0.001f;
+
+static const Ogre::Vector3 DEFAULT_FPS_POSITION = Ogre::Vector3(5, 5, 10);
+
+FrameViewController::FrameViewController()
+{
+  axis_property_ = new rviz_common::properties::EnumProperty("Point towards", fmtAxis(6),
+                                    "Point the camera along the given axis of the frame.", this,
+                                    SLOT(changedAxis()));
+  axis_property_->addOption(ANY_AXIS, -1);
+
+  // x,y,z axes get integers from 1..6: +x, -x, +y, -y, +z, -z
+  for (int i = 1; i <= 6; ++i) {
+    axis_property_->addOption(fmtAxis(i), i);
+  }
+  previous_axis_ = axis_property_->getOptionInt();
+
+  locked_property_ = new rviz_common::properties::BoolProperty("Lock Camera", false,
+    "Lock camera in its current pose relative to the frame", this);
+}
+
+void FrameViewController::onInitialize()
+{
+  FPSViewController::onInitialize();
+  changedAxis();
+}
+
+int FrameViewController::actualCameraAxisOption(double precision) const
+{
+  // compare current camera direction with unit axes
+  Ogre::Vector3 actual =
+    (camera_scene_node_->getOrientation() * ROBOT_TO_CAMERA_ROTATION.Inverse()) *
+    Ogre::Vector3::UNIT_X;
+  for (unsigned int i = 0; i < 3; ++i) {
+    Ogre::Vector3 axis(0, 0, 0);
+    axis[i] = 1.0;
+    auto scalar_product = axis.dotProduct(actual);
+    if (std::abs(scalar_product) > 1.0 - precision) {
+      return 1 + 2 * i + (scalar_product > 0 ? 0 : 1);
+    }
+  }
+  return -1;
+}
+
+void FrameViewController::setAxisFromCamera()
+{
+  int actual = actualCameraAxisOption();
+  if (axis_property_->getOptionInt() == actual) {  // no change?
+    return;
+  }
+
+  QSignalBlocker block(axis_property_);
+  axis_property_->setString(actual == -1 ? ANY_AXIS : fmtAxis(actual));
+  rememberAxis(actual);
+}
+
+void FrameViewController::changedAxis()
+{
+  rememberAxis(axis_property_->getOptionInt());
+  reset();
+}
+
+inline void FrameViewController::rememberAxis(int current)
+{
+  if (current >= 1) {  // remember previous axis selection
+    previous_axis_ = current;
+  }
+}
+
+void FrameViewController::reset()
+{
+  camera_scene_node_->setPosition(Ogre::Vector3::ZERO);
+  Ogre::Vector3 axis(0, 0, 0);
+  int option = previous_axis_;
+  if (option >= 1 && option <= 6) {
+    axis[(option - 1) / 2] = (option % 2) ? +1 : -1;
+    Ogre::Quaternion q;
+    if (option == 2) {  // special case for the -X axis
+      // Create a rotation of 180 degrees around the Z axis
+      q = Ogre::Quaternion(Ogre::Radian(Ogre::Math::PI), Ogre::Vector3::UNIT_Z);
+    } else {
+      q = Ogre::Vector3::UNIT_X.getRotationTo(axis);
+    }
+    camera_scene_node_->setOrientation(q * ROBOT_TO_CAMERA_ROTATION);
+  }
+  setPropertiesFromCamera(camera_);
+}
+
+void FrameViewController::handleMouseEvent(rviz_common::ViewportMouseEvent & event)
+{
+  if (locked_property_->getBool()) {
+    setStatus("Unlock camera in settings to enable mouse interaction.");
+    return;
+  }
+  FPSViewController::handleMouseEvent(event);
+}
+
+void FrameViewController::onTargetFrameChanged(
+  const Ogre::Vector3 & /*old_reference_position*/,
+  const Ogre::Quaternion & /*old_reference_orientation*/)
+{
+  // don't adapt the camera pose to the old reference position, but just jump to new frame
+}
+
+}  // namespace view_controllers
+}  // namespace rviz_default_plugins
+
+#include <pluginlib/class_list_macros.hpp>  // NOLINT(build/include_order)
+PLUGINLIB_EXPORT_CLASS(
+  rviz_default_plugins::view_controllers::FrameViewController, rviz_common::ViewController)
diff --git a/rviz_default_plugins/test/rviz_default_plugins/view_controllers/frame/frame_view_controller_test.cpp b/rviz_default_plugins/test/rviz_default_plugins/view_controllers/frame/frame_view_controller_test.cpp
new file mode 100644
index 000000000..e9555a14c
--- /dev/null
+++ b/rviz_default_plugins/test/rviz_default_plugins/view_controllers/frame/frame_view_controller_test.cpp
@@ -0,0 +1,251 @@
+// Copyright (c) 2018, Bosch Software Innovations GmbH.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//    * Redistributions of source code must retain the above copyright
+//      notice, this list of conditions and the following disclaimer.
+//
+//    * Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+//
+//    * Neither the name of the copyright holder nor the names of its
+//      contributors may be used to endorse or promote products derived from
+//      this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+
+#include <gmock/gmock.h>
+
+#include <memory>
+
+#include <QApplication>  // NOLINT cpplint cannot handle include order
+#include <QKeyEvent>  // NOLINT cpplint cannot handle include order
+
+#include <OgreCamera.h>
+#include <OgreQuaternion.h>
+#include <OgreRoot.h>
+#include <OgreVector.h>
+
+#include "rviz_common/render_panel.hpp"
+#include "rviz_common/viewport_mouse_event.hpp"
+#include "rviz_common/properties/enum_property.hpp"
+#include "rviz_common/properties/bool_property.hpp"
+
+#include "rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp"
+#include "rviz_default_plugins/view_controllers/orbit/orbit_view_controller.hpp"
+
+#include "../../displays/display_test_fixture.hpp"
+#include "../../scene_graph_introspection.hpp"
+#include "../view_controller_test_fixture.hpp"
+
+using namespace ::testing;  // NOLINT
+
+static const Ogre::Quaternion ROBOT_TO_CAMERA_ROTATION =
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Y) *
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Z);
+
+class FrameViewControllerTestFixture : public ViewControllerTestFixture
+{
+public:
+  FrameViewControllerTestFixture()
+  {
+    frame_ = std::make_shared<rviz_default_plugins::view_controllers::FrameViewController>();
+    frame_->initialize(context_.get());
+    testing_environment_->createOgreRenderWindow()->addViewport(frame_->getCamera());
+  }
+
+  void dragMouse(
+    int to_x, int to_y, int from_x, int from_y,
+    Qt::MouseButton button, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
+  {
+    dragMouseInViewport(frame_, to_x, to_y, from_x, from_y, button, modifiers);
+  }
+
+  int getAxisPropertyOptionInt()
+  {
+    auto axis_property = frame_->childAt(7);
+    EXPECT_THAT(axis_property->getNameStd(), StrEq("Point towards"));
+    auto enum_property = dynamic_cast<rviz_common::properties::EnumProperty *>(axis_property);
+    return enum_property->getOptionInt();
+  }
+
+
+  void setAxisPropertyOption(int option)
+  {
+    auto axis_property = frame_->childAt(7);
+    EXPECT_THAT(axis_property->getNameStd(), StrEq("Point towards"));
+    auto enum_property = dynamic_cast<rviz_common::properties::EnumProperty *>(axis_property);
+    ASSERT_THAT(enum_property, NotNull()) << "Property is not an EnumProperty";
+
+    auto axisString = [](int i) {
+        return QString("%1%2 axis").arg(QChar(i % 2 ? '+' : '-')).arg(QChar('x' + (i - 1) / 2));
+      };
+    // Use a direct approach: first store the current string
+    QString currentString = enum_property->getString();
+    // Set the property using the string value
+    enum_property->setString(axisString(option));
+
+    // Verify the option was set correctly
+    ASSERT_THAT(enum_property->getOptionInt(), Eq(option))
+      << "Failed to set axis property to option: " << option
+      << ", resulting optionInt: " << enum_property->getOptionInt();
+  }
+
+  bool getLockCameraProperty()
+  {
+    auto lock_property = frame_->childAt(8);
+    EXPECT_THAT(lock_property->getNameStd(), StrEq("Lock Camera"));
+    return static_cast<rviz_common::properties::BoolProperty *>(lock_property)->getBool();
+  }
+
+  void setLockCameraProperty(bool lock)
+  {
+    auto lock_property = frame_->childAt(8);
+    EXPECT_THAT(lock_property->getNameStd(), StrEq("Lock Camera"));
+    static_cast<rviz_common::properties::BoolProperty *>(lock_property)->setBool(lock);
+  }
+
+  void checkCameraLooksAlong(const Ogre::Vector3 & expected_direction, float tolerance = 0.001f)
+  {
+    Ogre::Vector3 camera_direction =
+      (frame_->getCamera()->getParentSceneNode()->getOrientation() *
+      ROBOT_TO_CAMERA_ROTATION.Inverse()) *
+      Ogre::Vector3::UNIT_X;
+
+    EXPECT_THAT(camera_direction.x, FloatNear(expected_direction.x, tolerance));
+    EXPECT_THAT(camera_direction.y, FloatNear(expected_direction.y, tolerance));
+    EXPECT_THAT(camera_direction.z, FloatNear(expected_direction.z, tolerance));
+  }
+
+  void setCameraToDefaultPosition()
+  {
+    auto yaw_property = frame_->childAt(4);
+    auto pitch_property = frame_->childAt(5);
+    ASSERT_THAT(yaw_property->getNameStd(), StrEq("Yaw"));
+    ASSERT_THAT(pitch_property->getNameStd(), StrEq("Pitch"));
+    yaw_property->setValue(0);  // set to zero to make result easier to check
+    pitch_property->setValue(0);  // set to zeor to make result easier to check
+    auto position_property = frame_->childAt(6);
+    ASSERT_THAT(position_property->getNameStd(), StrEq("Position"));
+    position_property->childAt(0)->setValue(0);
+    position_property->childAt(1)->setValue(0);
+    position_property->childAt(2)->setValue(0);
+    frame_->update(0, 0);  // Camera now looks in x-direction, located at the origin
+
+    EXPECT_THAT(yaw_property->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(pitch_property->getValue().toFloat(), FloatNear(0, 0.001f));
+    auto x_position = position_property->childAt(0);
+    auto y_position = position_property->childAt(1);
+    auto z_position = position_property->childAt(2);
+    EXPECT_THAT(x_position->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(y_position->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(z_position->getValue().toFloat(), FloatNear(0, 0.001f));
+  }
+
+  std::shared_ptr<rviz_default_plugins::view_controllers::FrameViewController> frame_;
+};
+
+TEST_F(FrameViewControllerTestFixture, locking_camera_prevents_mouse_interaction) {
+  setCameraToDefaultPosition();
+  setLockCameraProperty(true);
+  dragMouse(0, 10, 10, 10, Qt::LeftButton);
+
+  auto yaw_property = frame_->childAt(4);
+  auto pitch_property = frame_->childAt(5);
+
+  EXPECT_THAT(yaw_property->getValue().toFloat(), FloatNear(0, 0.001f));
+  EXPECT_THAT(pitch_property->getValue().toFloat(), FloatNear(0, 0.001f));
+}
+
+TEST_F(FrameViewControllerTestFixture, reset_points_camera_along_selected_axis) {
+  // Change camera orientation
+  auto yaw_property = frame_->childAt(4);
+  auto pitch_property = frame_->childAt(5);
+  yaw_property->setValue(1.5);
+  pitch_property->setValue(0.5);
+  frame_->update(0, 0);
+
+  // Reset should align with -z axis (option 6)
+  frame_->reset();
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+TEST_F(FrameViewControllerTestFixture, default_axis_is_negative_z) {
+  // Test that camera initially points along -z axis (option 6)
+  EXPECT_THAT(getAxisPropertyOptionInt(), Eq(6));
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_x_points_camera_along_x_axis) {
+  // +x axis = option 1
+  setAxisPropertyOption(1);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(1, 0, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_x_points_camera_along_negative_x_axis) {
+  // -x axis = option 2
+  setAxisPropertyOption(2);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(-1, 0, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_y_points_camera_along_y_axis) {
+  // +y axis = option 3
+  setAxisPropertyOption(3);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 1, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_y_points_camera_along_negative_y_axis) {
+  // -y axis = option 4
+  setAxisPropertyOption(4);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, -1, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_z_points_camera_along_z_axis) {
+  // +z axis = option 5
+  setAxisPropertyOption(5);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, 1));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_z_points_camera_along_negative_z_axis) {
+  // -z axis = option 6
+  setAxisPropertyOption(6);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+int main(int argc, char ** argv)
+{
+  QApplication app(argc, argv);
+  InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}


Reviews:
user: login: ahcorde
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:46:46Z
user: login: georgflick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:11:33Z
user: login: georgflick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:11:34Z
user: login: georgflick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:11:39Z
user: login: ahcorde
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:35:30Z

Review comments:
diff_hunk: @@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2012, Willow Garage, Inc.
+ * Copyright (c) 2018, Bosch Software Innovations GmbH.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the Willow Garage, Inc. nor the names of its
+ *       contributors may be used to endorse or promote products derived from
+ *       this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef RVIZ_DEFAULT_PLUGINS__VIEW_CONTROLLERS__FRAME__FRAME_VIEW_CONTROLLER_HPP_
+#define RVIZ_DEFAULT_PLUGINS__VIEW_CONTROLLERS__FRAME__FRAME_VIEW_CONTROLLER_HPP_
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wkeyword-macro"
+#endif
+
+#include <OgreVector.h>
+#include <OgreQuaternion.h>
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+#include "rviz_default_plugins/view_controllers/fps/fps_view_controller.hpp"
+
+namespace rviz_common
+{
+namespace properties
+{
+class FloatProperty;
+class Shape;
+class VectorProperty;
+class EnumProperty;
+class BoolProperty;
+}  // namespace properties
+}  // namespace rviz_common
+
+namespace rviz_default_plugins
+{
+namespace view_controllers
+{
+/** @brief A frame-aligned camera. */
+class RVIZ_DEFAULT_PLUGINS_PUBLIC FrameViewController : public FPSViewController
+{
+  Q_OBJECT
+
+public:
+  FrameViewController();
+
+  ~FrameViewController() override = default;
+
+  void onInitialize() override;
+
+  void handleMouseEvent(rviz_common::ViewportMouseEvent & evt) override;
+
+  void reset() override;
+
+  void yaw(float angle);
+
+  void pitch(float angle);
+
+protected:
+  void onTargetFrameChanged(
+    const Ogre::Vector3 & old_reference_position,
+    const Ogre::Quaternion & old_reference_orientation) override;
+
+  rviz_common::properties::EnumProperty * axis_property_;
path: rviz_default_plugins/include/rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp
user: login: ahcorde
body: add includes
created_at: 2025-05-02T13:43:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2009, Willow Garage, Inc.
+ * Copyright (c) 2018, Bosch Software Innovations GmbH.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the Willow Garage, Inc. nor the names of its
+ *       contributors may be used to endorse or promote products derived from
+ *       this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp"
+
+#include <OgreCamera.h>
+#include <OgreQuaternion.h>
+#include <OgreSceneNode.h>
+#include <OgreSceneManager.h>
+#include <OgreVector.h>
+#include <OgreViewport.h>
+
+#include "rviz_rendering/geometry.hpp"
+#include "rviz_rendering/objects/shape.hpp"
+
+#include "rviz_common/display_context.hpp"
+#include "rviz_common/properties/bool_property.hpp"
+#include "rviz_common/properties/enum_property.hpp"
+#include "rviz_common/viewport_mouse_event.hpp"
+#include "rviz_common/uniform_string_stream.hpp"
+
+
+namespace rviz_default_plugins
+{
+namespace view_controllers
+{
+static const QString ANY_AXIS("arbitrary");
+
+// helper function to create axis strings from option ID
+inline QString fmtAxis(int i)
+{
+  return QString("%1%2 axis").arg(QChar(i % 2 ? '+' : '-')).arg(QChar('x' + (i - 1) / 2));
+}
+
+static const Ogre::Quaternion ROBOT_TO_CAMERA_ROTATION =
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Y) *
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Z);
+
+static const float PITCH_LIMIT_LOW = -Ogre::Math::HALF_PI + 0.001f;
+static const float PITCH_LIMIT_HIGH = Ogre::Math::HALF_PI - 0.001f;
+
+static const Ogre::Vector3 DEFAULT_FPS_POSITION = Ogre::Vector3(5, 5, 10);
+
+FrameViewController::FrameViewController()
+{
+  axis_property_ = new rviz_common::properties::EnumProperty("Point towards", fmtAxis(6),
+                                    "Point the camera along the given axis of the frame.", this,
+                                    SLOT(changedAxis()));
+  axis_property_->addOption(ANY_AXIS, -1);
+
+  // x,y,z axes get integers from 1..6: +x, -x, +y, -y, +z, -z
+  for (int i = 1; i <= 6; ++i) {
+    axis_property_->addOption(fmtAxis(i), i);
+  }
+  previous_axis_ = axis_property_->getOptionInt();
+
+  locked_property_ = new rviz_common::properties::BoolProperty("Lock Camera", false,
+    "Lock camera in its current pose relative to the frame", this);
+}
+
+void FrameViewController::onInitialize()
+{
+  FPSViewController::onInitialize();
+  changedAxis();
+}
+
+int FrameViewController::actualCameraAxisOption(double precision) const
+{
+  // compare current camera direction with unit axes
+  Ogre::Vector3 actual =
+    (camera_scene_node_->getOrientation() * ROBOT_TO_CAMERA_ROTATION.Inverse()) *
+    Ogre::Vector3::UNIT_X;
+  for (unsigned int i = 0; i < 3; ++i) {
+    Ogre::Vector3 axis(0, 0, 0);
+    axis[i] = 1.0;
+    auto scalar_product = axis.dotProduct(actual);
+    if (std::abs(scalar_product) > 1.0 - precision) {
path: rviz_default_plugins/src/rviz_default_plugins/view_controllers/frame/frame_view_controller.cpp
user: login: ahcorde
body: include `<cmath>`
created_at: 2025-05-02T13:45:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+// Copyright (c) 2018, Bosch Software Innovations GmbH.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//    * Redistributions of source code must retain the above copyright
+//      notice, this list of conditions and the following disclaimer.
+//
+//    * Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+//
+//    * Neither the name of the copyright holder nor the names of its
+//      contributors may be used to endorse or promote products derived from
+//      this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+
+#include <gmock/gmock.h>
+
+#include <memory>
+
+#include <QApplication>  // NOLINT cpplint cannot handle include order
+#include <QKeyEvent>  // NOLINT cpplint cannot handle include order
+
+#include <OgreCamera.h>
+#include <OgreRoot.h>
+
+#include "rviz_common/render_panel.hpp"
+#include "rviz_common/viewport_mouse_event.hpp"
+#include "rviz_common/properties/enum_property.hpp"
+#include "rviz_common/properties/bool_property.hpp"
+
+#include "rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp"
+#include "rviz_default_plugins/view_controllers/orbit/orbit_view_controller.hpp"
+
+#include "../../displays/display_test_fixture.hpp"
+#include "../../scene_graph_introspection.hpp"
+#include "../view_controller_test_fixture.hpp"
+
+using namespace ::testing;  // NOLINT
+
+static const Ogre::Quaternion ROBOT_TO_CAMERA_ROTATION =
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Y) *
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Z);
+
+class FrameViewControllerTestFixture : public ViewControllerTestFixture
+{
+public:
+  FrameViewControllerTestFixture()
+  {
+    frame_ = std::make_shared<rviz_default_plugins::view_controllers::FrameViewController>();
+    frame_->initialize(context_.get());
+    testing_environment_->createOgreRenderWindow()->addViewport(frame_->getCamera());
+  }
+
+  void dragMouse(
+    int to_x, int to_y, int from_x, int from_y,
+    Qt::MouseButton button, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
+  {
+    dragMouseInViewport(frame_, to_x, to_y, from_x, from_y, button, modifiers);
+  }
+
+  int getAxisPropertyOptionInt()
+  {
+    auto axis_property = frame_->childAt(7);
+    EXPECT_THAT(axis_property->getNameStd(), StrEq("Point towards"));
+    auto enum_property = dynamic_cast<rviz_common::properties::EnumProperty *>(axis_property);
+    return enum_property->getOptionInt();
+  }
+
+
+  void setAxisPropertyOption(int option)
+  {
+    auto axis_property = frame_->childAt(7);
+    EXPECT_THAT(axis_property->getNameStd(), StrEq("Point towards"));
+    auto enum_property = dynamic_cast<rviz_common::properties::EnumProperty *>(axis_property);
+    ASSERT_THAT(enum_property, NotNull()) << "Property is not an EnumProperty";
+
+    auto axisString = [](int i) {
+        return QString("%1%2 axis").arg(QChar(i % 2 ? '+' : '-')).arg(QChar('x' + (i - 1) / 2));
+      };
+    // Use a direct approach: first store the current string
+    QString currentString = enum_property->getString();
+    // Set the property using the string value
+    enum_property->setString(axisString(option));
+
+    // Verify the option was set correctly
+    ASSERT_THAT(enum_property->getOptionInt(), Eq(option))
+      << "Failed to set axis property to option: " << option
+      << ", resulting optionInt: " << enum_property->getOptionInt();
+  }
+
+  bool getLockCameraProperty()
+  {
+    auto lock_property = frame_->childAt(8);
+    EXPECT_THAT(lock_property->getNameStd(), StrEq("Lock Camera"));
+    return static_cast<rviz_common::properties::BoolProperty *>(lock_property)->getBool();
+  }
+
+  void setLockCameraProperty(bool lock)
+  {
+    auto lock_property = frame_->childAt(8);
+    EXPECT_THAT(lock_property->getNameStd(), StrEq("Lock Camera"));
+    static_cast<rviz_common::properties::BoolProperty *>(lock_property)->setBool(lock);
+  }
+
+  void checkCameraLooksAlong(const Ogre::Vector3 & expected_direction, float tolerance = 0.001f)
+  {
+    Ogre::Vector3 camera_direction =
+      (frame_->getCamera()->getParentSceneNode()->getOrientation() *
+      ROBOT_TO_CAMERA_ROTATION.Inverse()) *
+      Ogre::Vector3::UNIT_X;
+
+    EXPECT_THAT(camera_direction.x, FloatNear(expected_direction.x, tolerance));
+    EXPECT_THAT(camera_direction.y, FloatNear(expected_direction.y, tolerance));
+    EXPECT_THAT(camera_direction.z, FloatNear(expected_direction.z, tolerance));
+  }
+
+  void setCameraToDefaultPosition()
+  {
+    auto yaw_property = frame_->childAt(4);
+    auto pitch_property = frame_->childAt(5);
+    ASSERT_THAT(yaw_property->getNameStd(), StrEq("Yaw"));
+    ASSERT_THAT(pitch_property->getNameStd(), StrEq("Pitch"));
+    yaw_property->setValue(0);  // set to zero to make result easier to check
+    pitch_property->setValue(0);  // set to zeor to make result easier to check
+    auto position_property = frame_->childAt(6);
+    ASSERT_THAT(position_property->getNameStd(), StrEq("Position"));
+    position_property->childAt(0)->setValue(0);
+    position_property->childAt(1)->setValue(0);
+    position_property->childAt(2)->setValue(0);
+    frame_->update(0, 0);  // Camera now looks in x-direction, located at the origin
+
+    EXPECT_THAT(yaw_property->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(pitch_property->getValue().toFloat(), FloatNear(0, 0.001f));
+    auto x_position = position_property->childAt(0);
+    auto y_position = position_property->childAt(1);
+    auto z_position = position_property->childAt(2);
+    EXPECT_THAT(x_position->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(y_position->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(z_position->getValue().toFloat(), FloatNear(0, 0.001f));
+  }
+
+  std::shared_ptr<rviz_default_plugins::view_controllers::FrameViewController> frame_;
+};
+
+TEST_F(FrameViewControllerTestFixture, locking_camera_prevents_mouse_interaction) {
+  setCameraToDefaultPosition();
+  setLockCameraProperty(true);
+  dragMouse(0, 10, 10, 10, Qt::LeftButton);
+
+  auto yaw_property = frame_->childAt(4);
+  auto pitch_property = frame_->childAt(5);
+
+  EXPECT_THAT(yaw_property->getValue().toFloat(), FloatNear(0, 0.001f));
+  EXPECT_THAT(pitch_property->getValue().toFloat(), FloatNear(0, 0.001f));
+}
+
+TEST_F(FrameViewControllerTestFixture, reset_points_camera_along_selected_axis) {
+  // Change camera orientation
+  auto yaw_property = frame_->childAt(4);
+  auto pitch_property = frame_->childAt(5);
+  yaw_property->setValue(1.5);
+  pitch_property->setValue(0.5);
+  frame_->update(0, 0);
+
+  // Reset should align with -z axis (option 6)
+  frame_->reset();
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+TEST_F(FrameViewControllerTestFixture, default_axis_is_negative_z) {
+  // Test that camera initially points along -z axis (option 6)
+  EXPECT_THAT(getAxisPropertyOptionInt(), Eq(6));
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_x_points_camera_along_x_axis) {
+  // +x axis = option 1
+  setAxisPropertyOption(1);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(1, 0, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_x_points_camera_along_negative_x_axis) {
+  // -x axis = option 2
+  setAxisPropertyOption(2);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(-1, 0, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_y_points_camera_along_y_axis) {
+  // +y axis = option 3
+  setAxisPropertyOption(3);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 1, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_y_points_camera_along_negative_y_axis) {
+  // -y axis = option 4
+  setAxisPropertyOption(4);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, -1, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_z_points_camera_along_z_axis) {
+  // +z axis = option 5
+  setAxisPropertyOption(5);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, 1));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_z_points_camera_along_negative_z_axis) {
+  // -z axis = option 6
+  setAxisPropertyOption(6);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+
+int main(int argc, char ** argv)
path: rviz_default_plugins/test/rviz_default_plugins/view_controllers/frame/frame_view_controller_test.cpp
user: login: ahcorde
body: ```suggestion
}

int main(int argc, char ** argv)
```
created_at: 2025-05-02T13:46:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+// Copyright (c) 2018, Bosch Software Innovations GmbH.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//    * Redistributions of source code must retain the above copyright
+//      notice, this list of conditions and the following disclaimer.
+//
+//    * Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+//
+//    * Neither the name of the copyright holder nor the names of its
+//      contributors may be used to endorse or promote products derived from
+//      this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+
+#include <gmock/gmock.h>
+
+#include <memory>
+
+#include <QApplication>  // NOLINT cpplint cannot handle include order
+#include <QKeyEvent>  // NOLINT cpplint cannot handle include order
+
+#include <OgreCamera.h>
+#include <OgreRoot.h>
+
+#include "rviz_common/render_panel.hpp"
+#include "rviz_common/viewport_mouse_event.hpp"
+#include "rviz_common/properties/enum_property.hpp"
+#include "rviz_common/properties/bool_property.hpp"
+
+#include "rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp"
+#include "rviz_default_plugins/view_controllers/orbit/orbit_view_controller.hpp"
+
+#include "../../displays/display_test_fixture.hpp"
+#include "../../scene_graph_introspection.hpp"
+#include "../view_controller_test_fixture.hpp"
+
+using namespace ::testing;  // NOLINT
+
+static const Ogre::Quaternion ROBOT_TO_CAMERA_ROTATION =
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Y) *
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Z);
+
+class FrameViewControllerTestFixture : public ViewControllerTestFixture
+{
+public:
+  FrameViewControllerTestFixture()
+  {
+    frame_ = std::make_shared<rviz_default_plugins::view_controllers::FrameViewController>();
+    frame_->initialize(context_.get());
+    testing_environment_->createOgreRenderWindow()->addViewport(frame_->getCamera());
+  }
+
+  void dragMouse(
+    int to_x, int to_y, int from_x, int from_y,
+    Qt::MouseButton button, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
+  {
+    dragMouseInViewport(frame_, to_x, to_y, from_x, from_y, button, modifiers);
+  }
+
+  int getAxisPropertyOptionInt()
+  {
+    auto axis_property = frame_->childAt(7);
+    EXPECT_THAT(axis_property->getNameStd(), StrEq("Point towards"));
+    auto enum_property = dynamic_cast<rviz_common::properties::EnumProperty *>(axis_property);
+    return enum_property->getOptionInt();
+  }
+
+
+  void setAxisPropertyOption(int option)
+  {
+    auto axis_property = frame_->childAt(7);
+    EXPECT_THAT(axis_property->getNameStd(), StrEq("Point towards"));
+    auto enum_property = dynamic_cast<rviz_common::properties::EnumProperty *>(axis_property);
+    ASSERT_THAT(enum_property, NotNull()) << "Property is not an EnumProperty";
+
+    auto axisString = [](int i) {
+        return QString("%1%2 axis").arg(QChar(i % 2 ? '+' : '-')).arg(QChar('x' + (i - 1) / 2));
+      };
+    // Use a direct approach: first store the current string
+    QString currentString = enum_property->getString();
+    // Set the property using the string value
+    enum_property->setString(axisString(option));
+
+    // Verify the option was set correctly
+    ASSERT_THAT(enum_property->getOptionInt(), Eq(option))
+      << "Failed to set axis property to option: " << option
+      << ", resulting optionInt: " << enum_property->getOptionInt();
+  }
+
+  bool getLockCameraProperty()
+  {
+    auto lock_property = frame_->childAt(8);
+    EXPECT_THAT(lock_property->getNameStd(), StrEq("Lock Camera"));
+    return static_cast<rviz_common::properties::BoolProperty *>(lock_property)->getBool();
+  }
+
+  void setLockCameraProperty(bool lock)
+  {
+    auto lock_property = frame_->childAt(8);
+    EXPECT_THAT(lock_property->getNameStd(), StrEq("Lock Camera"));
+    static_cast<rviz_common::properties::BoolProperty *>(lock_property)->setBool(lock);
+  }
+
+  void checkCameraLooksAlong(const Ogre::Vector3 & expected_direction, float tolerance = 0.001f)
+  {
+    Ogre::Vector3 camera_direction =
+      (frame_->getCamera()->getParentSceneNode()->getOrientation() *
+      ROBOT_TO_CAMERA_ROTATION.Inverse()) *
+      Ogre::Vector3::UNIT_X;
+
+    EXPECT_THAT(camera_direction.x, FloatNear(expected_direction.x, tolerance));
+    EXPECT_THAT(camera_direction.y, FloatNear(expected_direction.y, tolerance));
+    EXPECT_THAT(camera_direction.z, FloatNear(expected_direction.z, tolerance));
+  }
+
+  void setCameraToDefaultPosition()
+  {
+    auto yaw_property = frame_->childAt(4);
+    auto pitch_property = frame_->childAt(5);
+    ASSERT_THAT(yaw_property->getNameStd(), StrEq("Yaw"));
+    ASSERT_THAT(pitch_property->getNameStd(), StrEq("Pitch"));
+    yaw_property->setValue(0);  // set to zero to make result easier to check
+    pitch_property->setValue(0);  // set to zeor to make result easier to check
+    auto position_property = frame_->childAt(6);
+    ASSERT_THAT(position_property->getNameStd(), StrEq("Position"));
+    position_property->childAt(0)->setValue(0);
+    position_property->childAt(1)->setValue(0);
+    position_property->childAt(2)->setValue(0);
+    frame_->update(0, 0);  // Camera now looks in x-direction, located at the origin
+
+    EXPECT_THAT(yaw_property->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(pitch_property->getValue().toFloat(), FloatNear(0, 0.001f));
+    auto x_position = position_property->childAt(0);
+    auto y_position = position_property->childAt(1);
+    auto z_position = position_property->childAt(2);
+    EXPECT_THAT(x_position->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(y_position->getValue().toFloat(), FloatNear(0, 0.001f));
+    EXPECT_THAT(z_position->getValue().toFloat(), FloatNear(0, 0.001f));
+  }
+
+  std::shared_ptr<rviz_default_plugins::view_controllers::FrameViewController> frame_;
+};
+
+TEST_F(FrameViewControllerTestFixture, locking_camera_prevents_mouse_interaction) {
+  setCameraToDefaultPosition();
+  setLockCameraProperty(true);
+  dragMouse(0, 10, 10, 10, Qt::LeftButton);
+
+  auto yaw_property = frame_->childAt(4);
+  auto pitch_property = frame_->childAt(5);
+
+  EXPECT_THAT(yaw_property->getValue().toFloat(), FloatNear(0, 0.001f));
+  EXPECT_THAT(pitch_property->getValue().toFloat(), FloatNear(0, 0.001f));
+}
+
+TEST_F(FrameViewControllerTestFixture, reset_points_camera_along_selected_axis) {
+  // Change camera orientation
+  auto yaw_property = frame_->childAt(4);
+  auto pitch_property = frame_->childAt(5);
+  yaw_property->setValue(1.5);
+  pitch_property->setValue(0.5);
+  frame_->update(0, 0);
+
+  // Reset should align with -z axis (option 6)
+  frame_->reset();
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+TEST_F(FrameViewControllerTestFixture, default_axis_is_negative_z) {
+  // Test that camera initially points along -z axis (option 6)
+  EXPECT_THAT(getAxisPropertyOptionInt(), Eq(6));
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_x_points_camera_along_x_axis) {
+  // +x axis = option 1
+  setAxisPropertyOption(1);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(1, 0, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_x_points_camera_along_negative_x_axis) {
+  // -x axis = option 2
+  setAxisPropertyOption(2);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(-1, 0, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_y_points_camera_along_y_axis) {
+  // +y axis = option 3
+  setAxisPropertyOption(3);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 1, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_y_points_camera_along_negative_y_axis) {
+  // -y axis = option 4
+  setAxisPropertyOption(4);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, -1, 0));
+}
+
+TEST_F(FrameViewControllerTestFixture, setting_axis_to_positive_z_points_camera_along_z_axis) {
+  // +z axis = option 5
+  setAxisPropertyOption(5);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, 1));
+}
+
+TEST_F(FrameViewControllerTestFixture,
+  setting_axis_to_negative_z_points_camera_along_negative_z_axis) {
+  // -z axis = option 6
+  setAxisPropertyOption(6);
+  frame_->update(0, 0);
+
+  checkCameraLooksAlong(Ogre::Vector3(0, 0, -1));
+}
+
+
+int main(int argc, char ** argv)
path: rviz_default_plugins/test/rviz_default_plugins/view_controllers/frame/frame_view_controller_test.cpp
user: login: georgflick
body: done
created_at: 2025-05-02T14:11:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2009, Willow Garage, Inc.
+ * Copyright (c) 2018, Bosch Software Innovations GmbH.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the Willow Garage, Inc. nor the names of its
+ *       contributors may be used to endorse or promote products derived from
+ *       this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp"
+
+#include <OgreCamera.h>
+#include <OgreQuaternion.h>
+#include <OgreSceneNode.h>
+#include <OgreSceneManager.h>
+#include <OgreVector.h>
+#include <OgreViewport.h>
+
+#include "rviz_rendering/geometry.hpp"
+#include "rviz_rendering/objects/shape.hpp"
+
+#include "rviz_common/display_context.hpp"
+#include "rviz_common/properties/bool_property.hpp"
+#include "rviz_common/properties/enum_property.hpp"
+#include "rviz_common/viewport_mouse_event.hpp"
+#include "rviz_common/uniform_string_stream.hpp"
+
+
+namespace rviz_default_plugins
+{
+namespace view_controllers
+{
+static const QString ANY_AXIS("arbitrary");
+
+// helper function to create axis strings from option ID
+inline QString fmtAxis(int i)
+{
+  return QString("%1%2 axis").arg(QChar(i % 2 ? '+' : '-')).arg(QChar('x' + (i - 1) / 2));
+}
+
+static const Ogre::Quaternion ROBOT_TO_CAMERA_ROTATION =
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Y) *
+  Ogre::Quaternion(Ogre::Radian(-Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_Z);
+
+static const float PITCH_LIMIT_LOW = -Ogre::Math::HALF_PI + 0.001f;
+static const float PITCH_LIMIT_HIGH = Ogre::Math::HALF_PI - 0.001f;
+
+static const Ogre::Vector3 DEFAULT_FPS_POSITION = Ogre::Vector3(5, 5, 10);
+
+FrameViewController::FrameViewController()
+{
+  axis_property_ = new rviz_common::properties::EnumProperty("Point towards", fmtAxis(6),
+                                    "Point the camera along the given axis of the frame.", this,
+                                    SLOT(changedAxis()));
+  axis_property_->addOption(ANY_AXIS, -1);
+
+  // x,y,z axes get integers from 1..6: +x, -x, +y, -y, +z, -z
+  for (int i = 1; i <= 6; ++i) {
+    axis_property_->addOption(fmtAxis(i), i);
+  }
+  previous_axis_ = axis_property_->getOptionInt();
+
+  locked_property_ = new rviz_common::properties::BoolProperty("Lock Camera", false,
+    "Lock camera in its current pose relative to the frame", this);
+}
+
+void FrameViewController::onInitialize()
+{
+  FPSViewController::onInitialize();
+  changedAxis();
+}
+
+int FrameViewController::actualCameraAxisOption(double precision) const
+{
+  // compare current camera direction with unit axes
+  Ogre::Vector3 actual =
+    (camera_scene_node_->getOrientation() * ROBOT_TO_CAMERA_ROTATION.Inverse()) *
+    Ogre::Vector3::UNIT_X;
+  for (unsigned int i = 0; i < 3; ++i) {
+    Ogre::Vector3 axis(0, 0, 0);
+    axis[i] = 1.0;
+    auto scalar_product = axis.dotProduct(actual);
+    if (std::abs(scalar_product) > 1.0 - precision) {
path: rviz_default_plugins/src/rviz_default_plugins/view_controllers/frame/frame_view_controller.cpp
user: login: georgflick
body: done
created_at: 2025-05-02T14:11:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2012, Willow Garage, Inc.
+ * Copyright (c) 2018, Bosch Software Innovations GmbH.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the Willow Garage, Inc. nor the names of its
+ *       contributors may be used to endorse or promote products derived from
+ *       this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef RVIZ_DEFAULT_PLUGINS__VIEW_CONTROLLERS__FRAME__FRAME_VIEW_CONTROLLER_HPP_
+#define RVIZ_DEFAULT_PLUGINS__VIEW_CONTROLLERS__FRAME__FRAME_VIEW_CONTROLLER_HPP_
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wkeyword-macro"
+#endif
+
+#include <OgreVector.h>
+#include <OgreQuaternion.h>
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+#include "rviz_default_plugins/view_controllers/fps/fps_view_controller.hpp"
+
+namespace rviz_common
+{
+namespace properties
+{
+class FloatProperty;
+class Shape;
+class VectorProperty;
+class EnumProperty;
+class BoolProperty;
+}  // namespace properties
+}  // namespace rviz_common
+
+namespace rviz_default_plugins
+{
+namespace view_controllers
+{
+/** @brief A frame-aligned camera. */
+class RVIZ_DEFAULT_PLUGINS_PUBLIC FrameViewController : public FPSViewController
+{
+  Q_OBJECT
+
+public:
+  FrameViewController();
+
+  ~FrameViewController() override = default;
+
+  void onInitialize() override;
+
+  void handleMouseEvent(rviz_common::ViewportMouseEvent & evt) override;
+
+  void reset() override;
+
+  void yaw(float angle);
+
+  void pitch(float angle);
+
+protected:
+  void onTargetFrameChanged(
+    const Ogre::Vector3 & old_reference_position,
+    const Ogre::Quaternion & old_reference_orientation) override;
+
+  rviz_common::properties::EnumProperty * axis_property_;
path: rviz_default_plugins/include/rviz_default_plugins/view_controllers/frame/frame_view_controller.hpp
user: login: georgflick
body: done
created_at: 2025-05-02T14:11:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: ahcorde
created_at: 2025-05-06T08:49:35Z
body: Pulls: ros2/rviz#1453
Gist: https://gist.githubusercontent.com/ahcorde/299e3df66ab82feb6a1cc3abf46a2a28/raw/ef83b9bfa0968f4f804eb358dd8f4dcd1ae92c17/ros2.repos
BUILD args:  --packages-above-and-dependencies rviz_default_plugins
TEST args:  --packages-above rviz_default_plugins
ROS Distro: rolling
Job: ci_launcher
ci_launcher ran: https://ci.ros2.org/job/ci_launcher/15915
* Linux [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux&build=23441)](http://ci.ros2.org/job/ci_linux/23441/)
* Linux-aarch64 [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-aarch64&build=17627)](http://ci.ros2.org/job/ci_linux-aarch64/17627/)
* Linux-rhel [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-rhel&build=2899)](http://ci.ros2.org/job/ci_linux-rhel/2899/)
* Windows [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_windows&build=24275)](http://ci.ros2.org/job/ci_windows/24275/)
author_association: CONTRIBUTOR
user: login: ahcorde
created_at: 2025-05-07T08:07:48Z
body: https://github.com/Mergifyio backport kilted jazzy humble 
author_association: CONTRIBUTOR
user: login: mergify[bot]
created_at: 2025-05-07T08:07:59Z
body: > backport kilted jazzy humble

#### ✅ Backports have been created

<details>

* [#1458 Backported FrameAligned camera (backport #1453)](https://github.com/ros2/rviz/pull/1458) has been created for branch `kilted`
* [#1459 Backported FrameAligned camera (backport #1453)](https://github.com/ros2/rviz/pull/1459) has been created for branch `jazzy`
* [#1460 Backported FrameAligned camera (backport #1453)](https://github.com/ros2/rviz/pull/1460) has been created for branch `humble`

</details>


<!---
DO NOT EDIT
-*- Mergify Payload -*-
{"command": "backport kilted jazzy humble", "conclusion": "success", "action_is_running": true}
-*- Mergify Payload End -*-
-->
author_association: CONTRIBUTOR