diff --git a/drake_ros_viz/CMakeLists.txt b/drake_ros_viz/CMakeLists.txt index 79e44b49..d4eeded1 100644 --- a/drake_ros_viz/CMakeLists.txt +++ b/drake_ros_viz/CMakeLists.txt @@ -12,15 +12,23 @@ endif() find_package(ament_cmake_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(drake REQUIRED) +find_package(drake_ros_core REQUIRED) +find_package(drake_ros_tf2 REQUIRED) +find_package(rclcpp REQUIRED) find_package(visualization_msgs REQUIRED) add_library(drake_ros_viz + src/rviz_visualizer.cpp + src/scene_markers_system.cpp src/utilities/name_conventions.cpp src/utilities/type_conversion.cpp ) target_link_libraries(drake_ros_viz PUBLIC drake::drake + drake_ros_core::drake_ros_core + drake_ros_tf2::drake_ros_tf2 + rclcpp::rclcpp ${geometry_msgs_TARGETS} ${visualization_msgs_TARGETS} ) @@ -37,7 +45,10 @@ ament_export_libraries(drake_ros_viz) ament_export_targets(${PROJECT_NAME}) ament_export_dependencies(drake) +ament_export_dependencies(drake_ros_core) +ament_export_dependencies(drake_ros_tf2) ament_export_dependencies(geometry_msgs) +ament_export_dependencies(rclcpp) ament_export_dependencies(visualization_msgs) install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME} @@ -59,6 +70,14 @@ if(BUILD_TESTING) ament_cpplint() find_package(ament_cmake_gtest REQUIRED) + find_package(test_msgs REQUIRED) + + ament_add_gtest(test_scene_markers test/test_scene_markers.cpp) + target_link_libraries(test_scene_markers + drake::drake + drake_ros_viz + ${visualization_msgs_TARGETS} + ) ament_add_gtest(test_type_conversion test/test_type_conversion.cpp) target_link_libraries(test_type_conversion diff --git a/drake_ros_viz/include/drake_ros_viz/rviz_visualizer.hpp b/drake_ros_viz/include/drake_ros_viz/rviz_visualizer.hpp new file mode 100644 index 00000000..a89a3574 --- /dev/null +++ b/drake_ros_viz/include/drake_ros_viz/rviz_visualizer.hpp @@ -0,0 +1,74 @@ +// Copyright 2021 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef DRAKE_ROS_VIZ__RVIZ_VISUALIZER_HPP_ +#define DRAKE_ROS_VIZ__RVIZ_VISUALIZER_HPP_ + +#include +#include + +#include +#include +#include + +namespace drake_ros_viz { + +/// Set of parameters that configure an RvizVisualizer. +struct RvizVisualizerParams { + /// Publish triggers for scene markers and tf broadcasting. + std::unordered_set publish_triggers{ + drake::systems::TriggerType::kForced, + drake::systems::TriggerType::kPeriodic}; + + /// Period for periodic scene markers and tf broadcasting. + double publish_period{0.05}; + + /// Whether to perform tf broadcasting or not. + bool publish_tf{true}; +}; + +/// System for SceneGraph visualization in RViz. +/// +/// This system is a subdiagram aggregating a SceneMarkersSystem, a +/// RosPublisherSystem, and optionally a TfBroadcasterSystem to enable +/// SceneGraph visualization in RViz. All scene geometries are published +/// as a visualization_msgs/msg/MarkerArray message to a `/scene_markers` +/// ROS topic. If `publish_tf` is `true`, all SceneGraph frames are +/// broadcasted as tf2 transforms. +/// +/// It exports one input port: +/// - *graph_query* (abstract): expects a QueryObject from the SceneGraph. +class RvizVisualizer : public drake::systems::Diagram { + public: + explicit RvizVisualizer( + std::shared_ptr ros_interface, + RvizVisualizerParams params = {}); + + ~RvizVisualizer() override; + + // Forwarded to SceneTfSystem::RegisterMultibodyPlant + // and SceneTfBroadcasterSystem::RegisterMultibodyPlant + void RegisterMultibodyPlant( + const drake::multibody::MultibodyPlant* plant); + + const drake::systems::InputPort& get_graph_query_port() const; + + private: + class RvizVisualizerPrivate; + + std::unique_ptr impl_; +}; + +} // namespace drake_ros_viz + +#endif // DRAKE_ROS_VIZ__RVIZ_VISUALIZER_HPP_ diff --git a/drake_ros_viz/include/drake_ros_viz/scene_markers_system.hpp b/drake_ros_viz/include/drake_ros_viz/scene_markers_system.hpp new file mode 100644 index 00000000..efaaaea5 --- /dev/null +++ b/drake_ros_viz/include/drake_ros_viz/scene_markers_system.hpp @@ -0,0 +1,118 @@ +// Copyright 2021 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef DRAKE_ROS_VIZ__SCENE_MARKERS_SYSTEM_HPP_ +#define DRAKE_ROS_VIZ__SCENE_MARKERS_SYSTEM_HPP_ + +#include +#include // NOLINT(build/include_order) +#include + +#include +#include +#include +#include +#include + +namespace drake_ros_viz { + +/// Set of parameters that configure a SceneMarkersSystem. +struct SceneMarkersParams { + /// Configure SceneMarkersSystem to depict illustration geometries. + static SceneMarkersParams Illustration() { return {}; } + + /// Configure SceneMarkersSystem to depict proximity geometries. + static SceneMarkersParams Proximity() { + SceneMarkersParams params; + params.role = drake::geometry::Role::kProximity; + params.default_color.set(0.5, 0.5, 0.5, 1.0); + return params; + } + + /// Optional namespace for all markers, defaults + /// to utilities::GetMarkerNamespace() outcome. + std::optional marker_namespace{std::nullopt}; + + /// Role of the geometries to depict. + drake::geometry::Role role{drake::geometry::Role::kIllustration}; + + /// Default marker color if no ("phong", "diffuse") property is found. + drake::geometry::Rgba default_color{0.9, 0.9, 0.9, 1.0}; +}; + +/// System for SceneGraph depiction as a ROS markers array. +/// +/// This system outputs a `visualization_msgs/msg/MarkerArray` populated with +/// all geometries found in a SceneGraph, using Context time to timestamp +/// each `visualization_msgs/msg/Marker` message. +/// +/// It has one input port: +/// - *graph_query* (abstract): expects a QueryObject from the SceneGraph. +/// +/// It has one output port: +/// - *scene_markers* (abstract): all scene geometries, as a +/// visualization_msg::msg::MarkerArray message. +/// +/// This system provides the same base functionality in terms of SceneGraph +/// geometries lookup and message conversion for ROS-based applications as +/// the DrakeVisualizer system does for LCM-based applications. +/// Thus, discussions in DrakeVisualizer's documentation about geometry +/// versioning, geometry roles, and so on are equally applicable here. +class SceneMarkersSystem : public drake::systems::LeafSystem { + public: + explicit SceneMarkersSystem(SceneMarkersParams params = {}); + virtual ~SceneMarkersSystem(); + + /// Register a MultibodyPlant present in the scene + /** + * This provides the system with additional information to generate + * semantically meaningful frame string IDs and marker namespaces. + */ + void RegisterMultibodyPlant( + const drake::multibody::MultibodyPlant* plant); + + const SceneMarkersParams& params() const; + + const drake::systems::InputPort& get_graph_query_port() const; + + const drake::systems::OutputPort& get_markers_output_port() const; + + private: + // Outputs visualization_msgs::msg::MarkerArray message, + // timestamping the most up-to-date version. + void PopulateSceneMarkersMessage( + const drake::systems::Context& context, + visualization_msgs::msg::MarkerArray* output_value) const; + + // Returns cached visualization_msgs::msg::MarkerArray message, + // which is invalidated (and thus recomputed) upon a SceneGraph + // geometry version change. + const visualization_msgs::msg::MarkerArray& EvalSceneMarkers( + const drake::systems::Context& context, + bool* cached = nullptr) const; + + // Inspects the SceneGraph and carries out the conversion + // to visualization_msgs::msg::MarkerArray message unconditionally. + void CalcSceneMarkers( + const drake::systems::Context& context, + visualization_msgs::msg::MarkerArray* output_value) const; + + // PIMPL forward declaration + class SceneMarkersSystemPrivate; + + std::unique_ptr impl_; +}; + +} // namespace drake_ros_viz + +#endif // DRAKE_ROS_VIZ__SCENE_MARKERS_SYSTEM_HPP_ diff --git a/drake_ros_viz/package.xml b/drake_ros_viz/package.xml index c6e2db43..07363c5d 100644 --- a/drake_ros_viz/package.xml +++ b/drake_ros_viz/package.xml @@ -10,12 +10,16 @@ ament_cmake_ros + drake_ros_core + drake_ros_tf2 geometry_msgs + rclcpp visualization_msgs ament_cmake_clang_format ament_cmake_cpplint ament_cmake_gtest + test_msgs ament_cmake diff --git a/drake_ros_viz/src/rviz_visualizer.cpp b/drake_ros_viz/src/rviz_visualizer.cpp new file mode 100644 index 00000000..56f753ae --- /dev/null +++ b/drake_ros_viz/src/rviz_visualizer.cpp @@ -0,0 +1,101 @@ +// Copyright 2021 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "drake_ros_viz/rviz_visualizer.hpp" + +#include +#include +#include + +#include "drake_ros_viz/scene_markers_system.hpp" +#include +#include +#include +#include +#include + +namespace drake_ros_viz { + +class RvizVisualizer::RvizVisualizerPrivate { + public: + SceneMarkersSystem* scene_visual_markers; + SceneMarkersSystem* scene_collision_markers; + drake_ros_tf2::SceneTfBroadcasterSystem* scene_tf_broadcaster{nullptr}; +}; + +RvizVisualizer::RvizVisualizer( + std::shared_ptr ros_interface, + RvizVisualizerParams params) + : impl_(new RvizVisualizerPrivate()) { + drake::systems::DiagramBuilder builder; + + using drake_ros_core::RosPublisherSystem; + auto scene_visual_markers_publisher = builder.AddSystem( + RosPublisherSystem::Make( + "/scene_markers/visual", rclcpp::QoS(1), ros_interface, + params.publish_triggers, params.publish_period)); + + impl_->scene_visual_markers = + builder.AddSystem(SceneMarkersParams::Illustration()); + + builder.Connect(impl_->scene_visual_markers->get_markers_output_port(), + scene_visual_markers_publisher->get_input_port()); + + builder.ExportInput(impl_->scene_visual_markers->get_graph_query_port(), + "graph_query"); + + auto scene_collision_markers_publisher = builder.AddSystem( + RosPublisherSystem::Make( + "/scene_markers/collision", rclcpp::QoS(1), ros_interface, + params.publish_triggers, params.publish_period)); + + impl_->scene_collision_markers = + builder.AddSystem(SceneMarkersParams::Proximity()); + + builder.Connect(impl_->scene_collision_markers->get_markers_output_port(), + scene_collision_markers_publisher->get_input_port()); + + builder.ConnectInput("graph_query", + impl_->scene_collision_markers->get_graph_query_port()); + + if (params.publish_tf) { + impl_->scene_tf_broadcaster = + builder.AddSystem( + ros_interface, drake_ros_tf2::SceneTfBroadcasterParams{ + params.publish_triggers, params.publish_period}); + + builder.ConnectInput("graph_query", + impl_->scene_tf_broadcaster->get_graph_query_port()); + } + + builder.BuildInto(this); +} + +RvizVisualizer::~RvizVisualizer() {} + +void RvizVisualizer::RegisterMultibodyPlant( + const drake::multibody::MultibodyPlant* plant) { + impl_->scene_visual_markers->RegisterMultibodyPlant(plant); + impl_->scene_collision_markers->RegisterMultibodyPlant(plant); + if (impl_->scene_tf_broadcaster) { + impl_->scene_tf_broadcaster->RegisterMultibodyPlant(plant); + } +} + +const drake::systems::InputPort& RvizVisualizer::get_graph_query_port() + const { + return get_input_port(); +} + +} // namespace drake_ros_viz diff --git a/drake_ros_viz/src/scene_markers_system.cpp b/drake_ros_viz/src/scene_markers_system.cpp new file mode 100644 index 00000000..6e8a7faa --- /dev/null +++ b/drake_ros_viz/src/scene_markers_system.cpp @@ -0,0 +1,363 @@ +// Copyright 2021 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "drake_ros_viz/scene_markers_system.hpp" + +#include +#include + +#include "drake_ros_viz/utilities/name_conventions.hpp" +#include "drake_ros_viz/utilities/type_conversion.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace drake_ros_viz { + +namespace { + +class SceneGeometryToMarkers : public drake::geometry::ShapeReifier { + public: + DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(SceneGeometryToMarkers) + + explicit SceneGeometryToMarkers(const SceneMarkersParams& params) + : params_(params) {} + + ~SceneGeometryToMarkers() override = default; + + void Populate( + const drake::geometry::SceneGraphInspector& inspector, + const std::unordered_set*> + plants, + const drake::geometry::GeometryId& geometry_id, + visualization_msgs::msg::MarkerArray* marker_array) { + DRAKE_ASSERT(nullptr != marker_array); + marker_array_ = marker_array; + + prototype_marker_.header.frame_id = + drake_ros_tf2::utilities::GetTfFrameName(inspector, plants, + geometry_id); + prototype_marker_.ns = params_.marker_namespace.value_or( + utilities::GetMarkerNamespace(inspector, plants, geometry_id)); + prototype_marker_.id = marker_array_->markers.size(); + prototype_marker_.action = visualization_msgs::msg::Marker::MODIFY; + + prototype_marker_.lifetime = rclcpp::Duration::from_nanoseconds(0); + prototype_marker_.frame_locked = true; + + const drake::geometry::GeometryProperties* props = + inspector.GetProperties(geometry_id, params_.role); + DRAKE_ASSERT(nullptr != props); + drake::geometry::Rgba default_color = params_.default_color; + if (drake::geometry::Role::kIllustration != params_.role) { + const drake::geometry::IllustrationProperties* illustration_props = + inspector.GetIllustrationProperties(geometry_id); + if (illustration_props) { + default_color = illustration_props->GetPropertyOrDefault( + "phong", "diffuse", default_color); + } + } + const drake::geometry::Rgba& color = + props->GetPropertyOrDefault("phong", "diffuse", default_color); + prototype_marker_.color.r = color.r(); + prototype_marker_.color.g = color.g(); + prototype_marker_.color.b = color.b(); + prototype_marker_.color.a = color.a(); + + X_FG_ = inspector.GetPoseInFrame(geometry_id); + + inspector.GetShape(geometry_id).Reify(this); + } + + private: + using drake::geometry::ShapeReifier::ImplementGeometry; + + void ImplementGeometry(const drake::geometry::Sphere& sphere, + void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& marker = marker_array_->markers.back(); + marker.type = visualization_msgs::msg::Marker::SPHERE; + const double diameter = 2. * sphere.radius(); + marker.scale.x = diameter; + marker.scale.y = diameter; + marker.scale.z = diameter; + marker.pose = utilities::ToPoseMsg(X_FG_); + } + + void ImplementGeometry(const drake::geometry::Ellipsoid& ellipsoid, + void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& marker = marker_array_->markers.back(); + marker.type = visualization_msgs::msg::Marker::SPHERE; + marker.scale.x = 2. * ellipsoid.a(); + marker.scale.y = 2. * ellipsoid.b(); + marker.scale.z = 2. * ellipsoid.c(); + marker.pose = utilities::ToPoseMsg(X_FG_); + } + + void ImplementGeometry(const drake::geometry::Cylinder& cylinder, + void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& marker = marker_array_->markers.back(); + marker.type = visualization_msgs::msg::Marker::CYLINDER; + const double diameter = 2. * cylinder.radius(); + marker.scale.x = diameter; + marker.scale.y = diameter; + marker.scale.z = cylinder.length(); + marker.pose = utilities::ToPoseMsg(X_FG_); + } + + void ImplementGeometry(const drake::geometry::HalfSpace&, void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& marker = marker_array_->markers.back(); + marker.type = visualization_msgs::msg::Marker::CUBE; + constexpr double kHalfSpaceLength = 50.; + constexpr double kHalfSpaceThickness = 1.; + marker.scale.x = kHalfSpaceLength; + marker.scale.y = kHalfSpaceLength; + marker.scale.z = kHalfSpaceThickness; + const drake::math::RigidTransform X_GH{ + drake::Vector3{0., 0., -kHalfSpaceThickness / 2.}}; + const drake::math::RigidTransform X_FH = X_FG_ * X_GH; + marker.pose = utilities::ToPoseMsg(X_FH); + } + + void ImplementGeometry(const drake::geometry::Box& box, void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& marker = marker_array_->markers.back(); + marker.type = visualization_msgs::msg::Marker::CUBE; + marker.scale.x = box.width(); + marker.scale.y = box.depth(); + marker.scale.z = box.height(); + marker.pose = utilities::ToPoseMsg(X_FG_); + } + + void ImplementGeometry(const drake::geometry::Capsule& capsule, + void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& body_marker = + marker_array_->markers.back(); + body_marker.type = visualization_msgs::msg::Marker::CYLINDER; + const double diameter = 2. * capsule.radius(); + body_marker.scale.x = diameter; + body_marker.scale.y = diameter; + body_marker.scale.z = capsule.length(); + body_marker.pose = utilities::ToPoseMsg(X_FG_); + + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& upper_cap_marker = + marker_array_->markers.back(); + upper_cap_marker.id = body_marker.id + 1; + upper_cap_marker.type = visualization_msgs::msg::Marker::SPHERE; + upper_cap_marker.scale.x = diameter; + upper_cap_marker.scale.y = diameter; + upper_cap_marker.scale.z = diameter; + const drake::math::RigidTransform X_GU{ + drake::Vector3{0., 0., capsule.length() / 2.}}; + const drake::math::RigidTransform X_FU = X_FG_ * X_GU; + upper_cap_marker.pose = utilities::ToPoseMsg(X_FU); + + marker_array_->markers.push_back(upper_cap_marker); + + visualization_msgs::msg::Marker& lower_cap_marker = + marker_array_->markers.back(); + lower_cap_marker.id = upper_cap_marker.id + 1; + const drake::math::RigidTransform X_GL{ + drake::Vector3{0., 0., -capsule.length() / 2.}}; + const drake::math::RigidTransform X_FL = X_FG_ * X_GL; + lower_cap_marker.pose = utilities::ToPoseMsg(X_FL); + } + + void ImplementGeometry(const drake::geometry::Convex& convex, + void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& marker = marker_array_->markers.back(); + marker.type = visualization_msgs::msg::Marker::MESH_RESOURCE; + marker.scale.x = convex.scale(); + marker.scale.y = convex.scale(); + marker.scale.z = convex.scale(); + // Assume it is an absolute path and turn it into a file URL. + marker.mesh_resource = "file://" + convex.filename(); + marker.pose = utilities::ToPoseMsg(X_FG_); + } + + void ImplementGeometry(const drake::geometry::Mesh& mesh, void*) override { + marker_array_->markers.push_back(prototype_marker_); + + visualization_msgs::msg::Marker& marker = marker_array_->markers.back(); + marker.type = visualization_msgs::msg::Marker::MESH_RESOURCE; + marker.scale.x = mesh.scale(); + marker.scale.y = mesh.scale(); + marker.scale.z = mesh.scale(); + // Assume it is an absolute path and turn it into a file URL. + marker.mesh_resource = "file://" + mesh.filename(); + marker.pose = utilities::ToPoseMsg(X_FG_); + } + + const SceneMarkersParams& params_; + visualization_msgs::msg::MarkerArray* marker_array_{nullptr}; + visualization_msgs::msg::Marker prototype_marker_{}; + drake::math::RigidTransform X_FG_{}; +}; + +} // namespace + +class SceneMarkersSystem::SceneMarkersSystemPrivate { + public: + explicit SceneMarkersSystemPrivate(SceneMarkersParams _params) + : params(std::move(_params)) {} + + const SceneMarkersParams params; + drake::systems::CacheIndex scene_markers_cache_index; + drake::systems::InputPortIndex graph_query_port_index; + drake::systems::OutputPortIndex scene_markers_port_index; + std::unordered_set*> plants; + mutable drake::geometry::GeometryVersion version; +}; + +SceneMarkersSystem::SceneMarkersSystem(SceneMarkersParams params) + : impl_(new SceneMarkersSystemPrivate(std::move(params))) { + impl_->graph_query_port_index = + this->DeclareAbstractInputPort( + "graph_query", + drake::Value>{}) + .get_index(); + + impl_->scene_markers_cache_index = + this->DeclareCacheEntry("scene_markers_cache", + &SceneMarkersSystem::CalcSceneMarkers, + {nothing_ticket()}) + .cache_index(); + + impl_->scene_markers_port_index = + this->DeclareAbstractOutputPort( + "scene_markers", &SceneMarkersSystem::PopulateSceneMarkersMessage) + .get_index(); +} + +SceneMarkersSystem::~SceneMarkersSystem() {} + +void SceneMarkersSystem::RegisterMultibodyPlant( + const drake::multibody::MultibodyPlant* plant) { + DRAKE_THROW_UNLESS(plant != nullptr); + impl_->plants.insert(plant); +} + +namespace { + +visualization_msgs::msg::Marker MakeDeleteAllMarker() { + visualization_msgs::msg::Marker marker; + marker.action = visualization_msgs::msg::Marker::DELETEALL; + return marker; +} + +} // namespace + +void SceneMarkersSystem::PopulateSceneMarkersMessage( + const drake::systems::Context& context, + visualization_msgs::msg::MarkerArray* output_value) const { + bool cached; + *output_value = this->EvalSceneMarkers(context, &cached); + if (!cached) { + // Cache invalidated after scene change. + // Delete all pre-existing markers before an update. + output_value->markers.insert(output_value->markers.begin(), + MakeDeleteAllMarker()); + } + const builtin_interfaces::msg::Time stamp = + rclcpp::Time() + rclcpp::Duration::from_seconds(context.get_time()); + for (visualization_msgs::msg::Marker& marker : output_value->markers) { + marker.header.stamp = stamp; + } +} + +const visualization_msgs::msg::MarkerArray& +SceneMarkersSystem::EvalSceneMarkers( + const drake::systems::Context& context, bool* cached) const { + const drake::geometry::QueryObject& query_object = + get_input_port(impl_->graph_query_port_index) + .Eval>(context); + const drake::geometry::GeometryVersion& current_version = + query_object.inspector().geometry_version(); + const bool same_version = + impl_->version.IsSameAs(current_version, impl_->params.role); + if (!same_version) { + // Invalidate scene markers cache + get_cache_entry(impl_->scene_markers_cache_index) + .get_mutable_cache_entry_value(context) + .mark_out_of_date(); + impl_->version = current_version; + } + if (cached) { + *cached = same_version; + } + return get_cache_entry(impl_->scene_markers_cache_index) + .Eval(context); +} + +void SceneMarkersSystem::CalcSceneMarkers( + const drake::systems::Context& context, + visualization_msgs::msg::MarkerArray* output_value) const { + const drake::geometry::QueryObject& query_object = + get_input_port(impl_->graph_query_port_index) + .Eval>(context); + const drake::geometry::SceneGraphInspector& inspector = + query_object.inspector(); + output_value->markers.reserve( + inspector.NumGeometriesWithRole(impl_->params.role)); + for (const drake::geometry::FrameId& frame_id : inspector.GetAllFrameIds()) { + for (const drake::geometry::GeometryId& geometry_id : + inspector.GetGeometries(frame_id, impl_->params.role)) { + SceneGeometryToMarkers(impl_->params) + .Populate(inspector, impl_->plants, geometry_id, output_value); + } + } +} + +const SceneMarkersParams& SceneMarkersSystem::params() const { + return impl_->params; +} + +const drake::systems::InputPort& +SceneMarkersSystem::get_graph_query_port() const { + return get_input_port(impl_->graph_query_port_index); +} + +const drake::systems::OutputPort& +SceneMarkersSystem::get_markers_output_port() const { + return get_output_port(impl_->scene_markers_port_index); +} + +} // namespace drake_ros_viz diff --git a/drake_ros_viz/test/test_scene_markers.cpp b/drake_ros_viz/test/test_scene_markers.cpp new file mode 100644 index 00000000..0a80fd16 --- /dev/null +++ b/drake_ros_viz/test/test_scene_markers.cpp @@ -0,0 +1,553 @@ +// Copyright 2021 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include "drake_ros_viz/scene_markers_system.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using drake_ros_viz::SceneMarkersSystem; + +static constexpr char kSourceName[] = "test"; +static constexpr double kTolerance{1e-6}; + +struct SingleSphereSceneTestDetails { + static constexpr char kName[] = "sphere"; + static constexpr double kRadius{1.}; + + static drake::geometry::FramePoseVector PopulateSceneGraph( + const drake::geometry::SourceId& source_id, + drake::geometry::SceneGraph* scene_graph) { + const drake::geometry::GeometryId geometry_id = + scene_graph->RegisterAnchoredGeometry( + source_id, + std::make_unique( + drake::math::RigidTransform::Identity(), + std::make_unique(kRadius), kName)); + scene_graph->AssignRole(source_id, geometry_id, + drake::geometry::IllustrationProperties()); + return {}; + } + + static void CheckSceneMarkers( + const visualization_msgs::msg::MarkerArray& marker_array, + SceneMarkersSystem* scene_markers_system) { + ASSERT_EQ(marker_array.markers.size(), 2u); + const visualization_msgs::msg::Marker& control_marker = + marker_array.markers[0]; + EXPECT_EQ(control_marker.action, + visualization_msgs::msg::Marker::DELETEALL); + const visualization_msgs::msg::Marker& marker = marker_array.markers[1]; + EXPECT_EQ(marker.header.frame_id, "world"); + EXPECT_EQ(marker.header.stamp.sec, 0); + EXPECT_EQ(marker.header.stamp.nanosec, 0u); + std::stringstream ss; + ss << "/" << kSourceName << "/" << kName; + EXPECT_EQ(marker.ns, ss.str()); + EXPECT_EQ(marker.id, 0); + EXPECT_EQ(marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(marker.type, visualization_msgs::msg::Marker::SPHERE); + EXPECT_EQ(marker.lifetime.sec, 0); + EXPECT_EQ(marker.lifetime.nanosec, 0u); + EXPECT_TRUE(marker.frame_locked); + constexpr double kDiameter = 2. * kRadius; + EXPECT_DOUBLE_EQ(marker.scale.x, kDiameter); + EXPECT_DOUBLE_EQ(marker.scale.y, kDiameter); + EXPECT_DOUBLE_EQ(marker.scale.z, kDiameter); + const drake::geometry::Rgba& default_color = + scene_markers_system->params().default_color; + EXPECT_NEAR(marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.w, 1.); + } +}; + +struct SingleEllipsoidSceneTestDetails { + static constexpr char kName[] = "ellipsoid"; + static constexpr double kLengthA{0.3}; + static constexpr double kLengthB{0.4}; + static constexpr double kLengthC{0.5}; + + static drake::geometry::FramePoseVector PopulateSceneGraph( + const drake::geometry::SourceId& source_id, + drake::geometry::SceneGraph* scene_graph) { + const drake::geometry::GeometryId geometry_id = + scene_graph->RegisterAnchoredGeometry( + source_id, std::make_unique( + drake::math::RigidTransform::Identity(), + std::make_unique( + kLengthA, kLengthB, kLengthC), + kName)); + scene_graph->AssignRole(source_id, geometry_id, + drake::geometry::IllustrationProperties()); + return {}; + } + + static void CheckSceneMarkers( + const visualization_msgs::msg::MarkerArray& marker_array, + SceneMarkersSystem* scene_markers_system) { + ASSERT_EQ(marker_array.markers.size(), 2u); + const visualization_msgs::msg::Marker& control_marker = + marker_array.markers[0]; + EXPECT_EQ(control_marker.action, + visualization_msgs::msg::Marker::DELETEALL); + const visualization_msgs::msg::Marker& marker = marker_array.markers[1]; + EXPECT_EQ(marker.header.frame_id, "world"); + EXPECT_EQ(marker.header.stamp.sec, 0); + EXPECT_EQ(marker.header.stamp.nanosec, 0u); + std::stringstream ss; + ss << "/" << kSourceName << "/" << kName; + EXPECT_EQ(marker.ns, ss.str()); + EXPECT_EQ(marker.id, 0); + EXPECT_EQ(marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(marker.type, visualization_msgs::msg::Marker::SPHERE); + EXPECT_EQ(marker.lifetime.sec, 0); + EXPECT_EQ(marker.lifetime.nanosec, 0u); + EXPECT_TRUE(marker.frame_locked); + EXPECT_DOUBLE_EQ(marker.scale.x, 2. * kLengthA); + EXPECT_DOUBLE_EQ(marker.scale.y, 2. * kLengthB); + EXPECT_DOUBLE_EQ(marker.scale.z, 2. * kLengthC); + const drake::geometry::Rgba& default_color = + scene_markers_system->params().default_color; + EXPECT_NEAR(marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.w, 1.); + } +}; + +struct SingleCylinderSceneTestDetails { + static constexpr char kName[] = "cylinder"; + static constexpr double kRadius{0.5}; + static constexpr double kLength{1.0}; + + static drake::geometry::FramePoseVector PopulateSceneGraph( + const drake::geometry::SourceId& source_id, + drake::geometry::SceneGraph* scene_graph) { + const drake::geometry::GeometryId geometry_id = + scene_graph->RegisterAnchoredGeometry( + source_id, + std::make_unique( + drake::math::RigidTransform::Identity(), + std::make_unique(kRadius, kLength), + kName)); + scene_graph->AssignRole(source_id, geometry_id, + drake::geometry::IllustrationProperties()); + return {}; + } + + static void CheckSceneMarkers( + const visualization_msgs::msg::MarkerArray& marker_array, + SceneMarkersSystem* scene_markers_system) { + ASSERT_EQ(marker_array.markers.size(), 2u); + const visualization_msgs::msg::Marker& control_marker = + marker_array.markers[0]; + EXPECT_EQ(control_marker.action, + visualization_msgs::msg::Marker::DELETEALL); + const visualization_msgs::msg::Marker& marker = marker_array.markers[1]; + EXPECT_EQ(marker.header.frame_id, "world"); + EXPECT_EQ(marker.header.stamp.sec, 0); + EXPECT_EQ(marker.header.stamp.nanosec, 0u); + std::stringstream ss; + ss << "/" << kSourceName << "/" << kName; + EXPECT_EQ(marker.ns, ss.str()); + EXPECT_EQ(marker.id, 0); + EXPECT_EQ(marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(marker.type, visualization_msgs::msg::Marker::CYLINDER); + EXPECT_EQ(marker.lifetime.sec, 0); + EXPECT_EQ(marker.lifetime.nanosec, 0u); + EXPECT_TRUE(marker.frame_locked); + constexpr double kDiameter = 2. * kRadius; + EXPECT_DOUBLE_EQ(marker.scale.x, kDiameter); + EXPECT_DOUBLE_EQ(marker.scale.y, kDiameter); + EXPECT_DOUBLE_EQ(marker.scale.z, kLength); + const drake::geometry::Rgba& default_color = + scene_markers_system->params().default_color; + EXPECT_NEAR(marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.w, 1.); + } +}; + +struct SingleHalfSpaceSceneTestDetails { + static constexpr char kName[] = "hspace"; + + static drake::geometry::FramePoseVector PopulateSceneGraph( + const drake::geometry::SourceId& source_id, + drake::geometry::SceneGraph* scene_graph) { + const drake::geometry::GeometryId geometry_id = + scene_graph->RegisterAnchoredGeometry( + source_id, + std::make_unique( + drake::math::RigidTransform::Identity(), + std::make_unique(), kName)); + scene_graph->AssignRole(source_id, geometry_id, + drake::geometry::IllustrationProperties()); + return {}; + } + + static void CheckSceneMarkers( + const visualization_msgs::msg::MarkerArray& marker_array, + SceneMarkersSystem* scene_markers_system) { + ASSERT_EQ(marker_array.markers.size(), 2u); + const visualization_msgs::msg::Marker& control_marker = + marker_array.markers[0]; + EXPECT_EQ(control_marker.action, + visualization_msgs::msg::Marker::DELETEALL); + const visualization_msgs::msg::Marker& marker = marker_array.markers[1]; + EXPECT_EQ(marker.header.frame_id, "world"); + EXPECT_EQ(marker.header.stamp.sec, 0); + EXPECT_EQ(marker.header.stamp.nanosec, 0u); + std::stringstream ss; + ss << "/" << kSourceName << "/" << kName; + EXPECT_EQ(marker.ns, ss.str()); + EXPECT_EQ(marker.id, 0); + EXPECT_EQ(marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(marker.type, visualization_msgs::msg::Marker::CUBE); + EXPECT_EQ(marker.lifetime.sec, 0); + EXPECT_EQ(marker.lifetime.nanosec, 0u); + EXPECT_TRUE(marker.frame_locked); + EXPECT_GT(marker.scale.x, 10.0); + EXPECT_GT(marker.scale.y, 10.0); + const drake::geometry::Rgba& default_color = + scene_markers_system->params().default_color; + EXPECT_NEAR(marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(marker.color.a, default_color.a(), kTolerance); + } +}; + +struct SingleBoxSceneTestDetails { + static constexpr char kName[] = "box"; + static constexpr double kWidth{0.5}; + static constexpr double kDepth{0.25}; + static constexpr double kHeight{1.0}; + + static drake::geometry::FramePoseVector PopulateSceneGraph( + const drake::geometry::SourceId& source_id, + drake::geometry::SceneGraph* scene_graph) { + const drake::geometry::GeometryId geometry_id = + scene_graph->RegisterAnchoredGeometry( + source_id, + std::make_unique( + drake::math::RigidTransform::Identity(), + std::make_unique(kWidth, kDepth, kHeight), + kName)); + scene_graph->AssignRole(source_id, geometry_id, + drake::geometry::IllustrationProperties()); + return {}; + } + + static void CheckSceneMarkers( + const visualization_msgs::msg::MarkerArray& marker_array, + SceneMarkersSystem* scene_markers_system) { + ASSERT_EQ(marker_array.markers.size(), 2u); + const visualization_msgs::msg::Marker& control_marker = + marker_array.markers[0]; + EXPECT_EQ(control_marker.action, + visualization_msgs::msg::Marker::DELETEALL); + const visualization_msgs::msg::Marker& marker = marker_array.markers[1]; + EXPECT_EQ(marker.header.frame_id, "world"); + EXPECT_EQ(marker.header.stamp.sec, 0); + EXPECT_EQ(marker.header.stamp.nanosec, 0u); + std::stringstream ss; + ss << "/" << kSourceName << "/" << kName; + EXPECT_EQ(marker.ns, ss.str()); + EXPECT_EQ(marker.id, 0); + EXPECT_EQ(marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(marker.type, visualization_msgs::msg::Marker::CUBE); + EXPECT_EQ(marker.lifetime.sec, 0); + EXPECT_EQ(marker.lifetime.nanosec, 0u); + EXPECT_TRUE(marker.frame_locked); + EXPECT_DOUBLE_EQ(marker.scale.x, kWidth); + EXPECT_DOUBLE_EQ(marker.scale.y, kDepth); + EXPECT_DOUBLE_EQ(marker.scale.z, kHeight); + const drake::geometry::Rgba& default_color = + scene_markers_system->params().default_color; + EXPECT_NEAR(marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.w, 1.); + } +}; + +struct SingleCapsuleSceneTestDetails { + static constexpr char kName[] = "capsule"; + static constexpr double kRadius{0.25}; + static constexpr double kLength{0.5}; + + static drake::geometry::FramePoseVector PopulateSceneGraph( + const drake::geometry::SourceId& source_id, + drake::geometry::SceneGraph* scene_graph) { + const drake::geometry::GeometryId geometry_id = + scene_graph->RegisterAnchoredGeometry( + source_id, + std::make_unique( + drake::math::RigidTransform::Identity(), + std::make_unique(kRadius, kLength), + kName)); + scene_graph->AssignRole(source_id, geometry_id, + drake::geometry::IllustrationProperties()); + return {}; + } + + static void CheckSceneMarkers( + const visualization_msgs::msg::MarkerArray& marker_array, + SceneMarkersSystem* scene_markers_system) { + ASSERT_EQ(marker_array.markers.size(), 4u); + const visualization_msgs::msg::Marker& control_marker = + marker_array.markers[0]; + EXPECT_EQ(control_marker.action, + visualization_msgs::msg::Marker::DELETEALL); + + const drake::geometry::Rgba& default_color = + scene_markers_system->params().default_color; + + const visualization_msgs::msg::Marker& body_marker = + marker_array.markers[1]; + EXPECT_EQ(body_marker.header.frame_id, "world"); + EXPECT_EQ(body_marker.header.stamp.sec, 0); + EXPECT_EQ(body_marker.header.stamp.nanosec, 0u); + std::stringstream ss; + ss << "/" << kSourceName << "/" << kName; + EXPECT_EQ(body_marker.ns, ss.str()); + EXPECT_EQ(body_marker.id, 0); + EXPECT_EQ(body_marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(body_marker.type, visualization_msgs::msg::Marker::CYLINDER); + EXPECT_EQ(body_marker.lifetime.sec, 0); + EXPECT_EQ(body_marker.lifetime.nanosec, 0u); + EXPECT_TRUE(body_marker.frame_locked); + constexpr double kDiameter = 2. * kRadius; + EXPECT_DOUBLE_EQ(body_marker.scale.x, kDiameter); + EXPECT_DOUBLE_EQ(body_marker.scale.y, kDiameter); + EXPECT_DOUBLE_EQ(body_marker.scale.z, kLength); + EXPECT_NEAR(body_marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(body_marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(body_marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(body_marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(body_marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(body_marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(body_marker.pose.position.z, 0.); + EXPECT_DOUBLE_EQ(body_marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(body_marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(body_marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(body_marker.pose.orientation.w, 1.); + + const visualization_msgs::msg::Marker& upper_cap_marker = + marker_array.markers[2]; + EXPECT_EQ(upper_cap_marker.header.frame_id, "world"); + EXPECT_EQ(upper_cap_marker.header.stamp.sec, 0); + EXPECT_EQ(upper_cap_marker.header.stamp.nanosec, 0u); + EXPECT_EQ(upper_cap_marker.ns, ss.str()); + EXPECT_EQ(upper_cap_marker.id, 1); + EXPECT_EQ(upper_cap_marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(upper_cap_marker.type, visualization_msgs::msg::Marker::SPHERE); + EXPECT_EQ(upper_cap_marker.lifetime.sec, 0); + EXPECT_EQ(upper_cap_marker.lifetime.nanosec, 0u); + EXPECT_TRUE(upper_cap_marker.frame_locked); + EXPECT_DOUBLE_EQ(upper_cap_marker.scale.x, kDiameter); + EXPECT_DOUBLE_EQ(upper_cap_marker.scale.y, kDiameter); + EXPECT_DOUBLE_EQ(upper_cap_marker.scale.z, kDiameter); + EXPECT_NEAR(upper_cap_marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(upper_cap_marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(upper_cap_marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(upper_cap_marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(upper_cap_marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(upper_cap_marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(upper_cap_marker.pose.position.z, kLength / 2); + EXPECT_DOUBLE_EQ(upper_cap_marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(upper_cap_marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(upper_cap_marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(upper_cap_marker.pose.orientation.w, 1.); + + const visualization_msgs::msg::Marker& lower_cap_marker = + marker_array.markers[3]; + EXPECT_EQ(lower_cap_marker.header.frame_id, "world"); + EXPECT_EQ(lower_cap_marker.header.stamp.sec, 0); + EXPECT_EQ(lower_cap_marker.header.stamp.nanosec, 0u); + EXPECT_EQ(lower_cap_marker.ns, ss.str()); + EXPECT_EQ(lower_cap_marker.id, 2); + EXPECT_EQ(lower_cap_marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(lower_cap_marker.type, visualization_msgs::msg::Marker::SPHERE); + EXPECT_EQ(lower_cap_marker.lifetime.sec, 0); + EXPECT_EQ(lower_cap_marker.lifetime.nanosec, 0u); + EXPECT_TRUE(lower_cap_marker.frame_locked); + EXPECT_DOUBLE_EQ(lower_cap_marker.scale.x, kDiameter); + EXPECT_DOUBLE_EQ(lower_cap_marker.scale.y, kDiameter); + EXPECT_DOUBLE_EQ(lower_cap_marker.scale.z, kDiameter); + EXPECT_NEAR(lower_cap_marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(lower_cap_marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(lower_cap_marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(lower_cap_marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(lower_cap_marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(lower_cap_marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(lower_cap_marker.pose.position.z, -kLength / 2); + EXPECT_DOUBLE_EQ(lower_cap_marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(lower_cap_marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(lower_cap_marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(lower_cap_marker.pose.orientation.w, 1.); + } +}; + +template +struct SingleMeshSceneTestDetails { + static constexpr char kName[] = "mesh"; + static constexpr char kFilename[] = "/tmp/dummy.obj"; + static constexpr double kScale{0.1}; + + static drake::geometry::FramePoseVector PopulateSceneGraph( + const drake::geometry::SourceId& source_id, + drake::geometry::SceneGraph* scene_graph) { + const drake::geometry::GeometryId geometry_id = + scene_graph->RegisterAnchoredGeometry( + source_id, std::make_unique( + drake::math::RigidTransform::Identity(), + std::make_unique(kFilename, kScale), kName)); + scene_graph->AssignRole(source_id, geometry_id, + drake::geometry::IllustrationProperties()); + return {}; + } + + static void CheckSceneMarkers( + const visualization_msgs::msg::MarkerArray& marker_array, + SceneMarkersSystem* scene_markers_system) { + ASSERT_EQ(marker_array.markers.size(), 2u); + const visualization_msgs::msg::Marker& control_marker = + marker_array.markers[0]; + EXPECT_EQ(control_marker.action, + visualization_msgs::msg::Marker::DELETEALL); + const visualization_msgs::msg::Marker& marker = marker_array.markers[1]; + EXPECT_EQ(marker.header.frame_id, "world"); + EXPECT_EQ(marker.header.stamp.sec, 0); + EXPECT_EQ(marker.header.stamp.nanosec, 0u); + std::stringstream ss; + ss << "/" << kSourceName << "/" << kName; + EXPECT_EQ(marker.ns, ss.str()); + EXPECT_EQ(marker.id, 0); + EXPECT_EQ(marker.action, visualization_msgs::msg::Marker::MODIFY); + EXPECT_EQ(marker.type, visualization_msgs::msg::Marker::MESH_RESOURCE); + EXPECT_EQ(marker.lifetime.sec, 0); + EXPECT_EQ(marker.lifetime.nanosec, 0u); + EXPECT_TRUE(marker.frame_locked); + EXPECT_EQ(marker.mesh_resource, std::string("file://") + kFilename); + EXPECT_DOUBLE_EQ(marker.scale.x, kScale); + EXPECT_DOUBLE_EQ(marker.scale.y, kScale); + EXPECT_DOUBLE_EQ(marker.scale.z, kScale); + const drake::geometry::Rgba& default_color = + scene_markers_system->params().default_color; + EXPECT_NEAR(marker.color.r, default_color.r(), kTolerance); + EXPECT_NEAR(marker.color.g, default_color.g(), kTolerance); + EXPECT_NEAR(marker.color.b, default_color.b(), kTolerance); + EXPECT_NEAR(marker.color.a, default_color.a(), kTolerance); + EXPECT_DOUBLE_EQ(marker.pose.position.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.position.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.x, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.y, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.z, 0.); + EXPECT_DOUBLE_EQ(marker.pose.orientation.w, 1.); + } +}; + +template +class SceneMarkersTest : public ::testing::Test {}; + +TYPED_TEST_SUITE_P(SceneMarkersTest); + +TYPED_TEST_P(SceneMarkersTest, NominalCase) { + using TestDetails = TypeParam; + + drake::systems::DiagramBuilder builder; + auto scene_graph = builder.AddSystem(); + auto source_id = scene_graph->RegisterSource(kSourceName); + auto pose_vector_value = drake::AbstractValue::Make( + TestDetails::PopulateSceneGraph(source_id, scene_graph)); + auto pose_vector_source = + builder.AddSystem( + *pose_vector_value); + builder.Connect(pose_vector_source->get_output_port(), + scene_graph->get_source_pose_port(source_id)); + + auto scene_markers = builder.AddSystem(); + builder.Connect(scene_graph->get_query_output_port(), + scene_markers->get_graph_query_port()); + + builder.ExportOutput(scene_markers->get_markers_output_port()); + + std::unique_ptr> diagram = builder.Build(); + std::unique_ptr> context = + diagram->CreateDefaultContext(); + + const drake::systems::OutputPort& markers_port = + diagram->get_output_port(); + auto marker_array = + markers_port.Eval(*context); + + TestDetails::CheckSceneMarkers(marker_array, scene_markers); +} + +REGISTER_TYPED_TEST_SUITE_P(SceneMarkersTest, NominalCase); + +using SingleGeometrySceneTestDetails = ::testing::Types< + SingleSphereSceneTestDetails, SingleEllipsoidSceneTestDetails, + SingleCylinderSceneTestDetails, SingleHalfSpaceSceneTestDetails, + SingleBoxSceneTestDetails, SingleCapsuleSceneTestDetails, + SingleMeshSceneTestDetails, + SingleMeshSceneTestDetails>; + +INSTANTIATE_TYPED_TEST_SUITE_P(SingleGeometrySceneMarkersTests, + SceneMarkersTest, + SingleGeometrySceneTestDetails);