Skip to content

Commit 7422bb4

Browse files
Feature/pnp solver develop (#12)
* feat(solve_pnp): add OpenCV to ROS camera coordinate system conversion Implement transformation logic to convert camera coordinates from the OpenCV standard (Z forward, X right, Y down) to the ROS standard (X forward, Y left, Z up). - The PnP solver output (relative pose) is now consistently converted and reported in the ROS camera frame. - Add unit tests for the coordinate transformation function. - Integrate simulated dynamic transforms to test and verify the robustness of the coordinate frame transformations during runtime. * feat(aim): implement inter-system communication and PnP pipeline visualization Successfully established communication link between the control system and the aiming module. This enables data exchange for target tracking and control. The full PnP (Perspective-n-Point) solving pipeline has been integrated and is running. The resulting pose (translation and rotation) is now visualized in the debugging interface to assist in development. Note: The output coordinate frame for the PnP solution is currently uncalibrated and requires further refinement/tuning to align with the world coordinate system. * feat(capturer): implement local video stream acquisition Successfully implemented the functionality to acquire and process video streams from local storage or device. * fix(video, pnp): resolve LocalVideo bug and correct PnP point ordering - Corrected a critical bug in the `LocalVideo` module . - Fixed an issue in the `SolvePnp` function where the ordering of 2D image points and their corresponding 3D object points was mismatched, leading to incorrect pose estimation results. The PnP output is now using the correct point correspondence. Further dedicated unit tests for the `SolvePnp` function need to be added in the next step to ensure robustness. * test(pnp): SolvePnp verified and accuracy meets specification The PnP (Perspective-n-Point) solver has passed comprehensive unit testing for various target poses and input data sets. The measured positional and rotational errors are consistently maintained within the required 5% tolerance, confirming the numerical stability and accuracy of the current PnP implementation. * docs(utility): update documentation for utility functions * fix(deps, pnp): resolve cv2eigen dependency and correct identifier point ordering - Corrected the dependency issue related to conversion functions, ensuring seamless data exchange between OpenCV and Eigen types. - Rectified the misplacement/misordering issue with the four identifier points used in the pose estimation pipeline. This ensures that the 2D image points and their corresponding 3D object points are correctly matched. Note: The core PnP (Perspective-n-Point) pose estimation accuracy issue is still under investigation and has not yet been resolved in this commit. * fix(coords, pnp): correct coordinate system transformation error Resolved the critical issue in coordinate system transformation logic that was causing inaccuracies in pose estimation. The fix ensures the proper conversion between the OpenCV and ROS camera coordinate definitions, resulting in a correct and stable output from the PnP (Perspective-n-Point) solver. The PnP pipeline is now fully operational and yielding expected results. * refactor(pnp): decouple PnP solver implementation and visualization The PnP (Perspective-n-Point) solver implementation has been separated from its visualization logic. - The `solve_pnp` module now strictly focuses on calculating the 3D pose (rotation and translation) and returns the numerical result. - All drawing, rendering, and coordinate frame visualization code has been moved to a dedicated `visualization` or `debug` module. * test(pnp): add test file with real-world data and establish performance baseline Implemented a dedicated test file utilizing actual captured data to validate the PnP (Perspective-n-Point) solver's output. The current performance baseline using this real-world data is recorded as: - **Distance Error (within 3m):** 8% - **Angular Error:** 15% * Update utils * Update doc and fix init bug of shared memory util * Update doc * feat(solve_pnp): add OpenCV to ROS camera coordinate system conversion Implement transformation logic to convert camera coordinates from the OpenCV standard (Z forward, X right, Y down) to the ROS standard (X forward, Y left, Z up). - The PnP solver output (relative pose) is now consistently converted and reported in the ROS camera frame. - Add unit tests for the coordinate transformation function. - Integrate simulated dynamic transforms to test and verify the robustness of the coordinate frame transformations during runtime. * feat(aim): implement inter-system communication and PnP pipeline visualization Successfully established communication link between the control system and the aiming module. This enables data exchange for target tracking and control. The full PnP (Perspective-n-Point) solving pipeline has been integrated and is running. The resulting pose (translation and rotation) is now visualized in the debugging interface to assist in development. Note: The output coordinate frame for the PnP solution is currently uncalibrated and requires further refinement/tuning to align with the world coordinate system. * feat(capturer): implement local video stream acquisition Successfully implemented the functionality to acquire and process video streams from local storage or device. * fix(video, pnp): resolve LocalVideo bug and correct PnP point ordering - Corrected a critical bug in the `LocalVideo` module . - Fixed an issue in the `SolvePnp` function where the ordering of 2D image points and their corresponding 3D object points was mismatched, leading to incorrect pose estimation results. The PnP output is now using the correct point correspondence. Further dedicated unit tests for the `SolvePnp` function need to be added in the next step to ensure robustness. * refactor(pnp): decouple PnP solver implementation and visualization The PnP (Perspective-n-Point) solver implementation has been separated from its visualization logic. - The `solve_pnp` module now strictly focuses on calculating the 3D pose (rotation and translation) and returns the numerical result. - All drawing, rendering, and coordinate frame visualization code has been moved to a dedicated `visualization` or `debug` module. * test(pnp): add test file with real-world data and establish performance baseline Implemented a dedicated test file utilizing actual captured data to validate the PnP (Perspective-n-Point) solver's output. The current performance baseline using this real-world data is recorded as: - **Distance Error (within 3m):** 8% - **Angular Error:** 15% * chore(deps): synchronize with upstream main branch Pulled latest changes from the main branch to ensure the current feature branch is up-to-date with recent bug fixes and dependencies updates. * Update doc/utility.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update src/kernel/visualization.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix(pose): remove redundant visual_armors variable * fix(video): modify LocalVideo function signatures for interface compatibility * Update src/module/debug/visualization/armor_visualizer.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix(visual):reuse publisher creation function and remove redundancy * chore(deps): synchronize with upstream main branch * chore(cmake): remove redundant module from test/CMakeLists.txt * feat(pnp): implement failure handling logic for PnP solver * chore(tooling): add resource download script for testing assets * feat(test): implement YAML configuration for test resources and integrate into CI Implemented a robust configuration system using YAML files to specify all necessary test resources. - **Resource Configuration:** Resources for testing `model_infer` and `solve_pnp` are now loaded dynamically from a centralized YAML configuration file. - **Test Adaptation:** Adapted `model_infer` and `solve_pnp` tests to consume resources based on the new configuration. - **CI Integration:** The new configuration-driven tests have been integrated into the automated testing pipeline (CI/CD process). - **Environment Support:** The configuration supports loading environment variables for sensitive paths or using pre-defined default values, enhancing deployment flexibility. * chore(ci): supply missing dependencies in CI/CD pipeline * fix(visual_armor,test,utility): minor fixes and general cleanup * fix(pnp): correct 4-point order and decouple PnP failure handling & visualization * refactor(visualization): remove redundant visualization variables * fix(test): correct visualization test by assigning unique armor IDs * refactor(cleanup): extensive cleanup of documentation, interfaces, and build configs Performed an extensive refactoring pass across the repository to improve code hygiene, interface consistency, and dependency management: 1. **Documentation Cleanup:** Removed the redundant documentation file `doc/utility.md` and updated related comments referencing its content. 2. **Interface Simplification:** Simplified the interface of the PnP solver (`solve_pnp`) by streamlining its method signatures and inputs. 3. **Code Consistency:** Unified constructor signatures across related classes for improved consistency and ease of use. 4. **Dependency Fix:** Corrected instances of superfluous dependency leakage, ensuring modules only link required libraries. 5. **Build Config Correction:** Extracted incorrectly merged/included content from `test/CMakeLists.txt` that did not belong to the test configuration. * feat(test): introduce AssetsManager and update resource URLs * fix(pnp): add error handling for solver failures and resolve minor issues * Update test/assets_manager.hpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Cleanup code * Cleanup script --------- Co-authored-by: heyeuu Co-authored-by: creeper5820 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 8687c42 commit 7422bb4

39 files changed

Lines changed: 1262 additions & 570 deletions

.github/workflows/gtest.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ jobs:
1616
env:
1717
WS_DIR: ${{ github.workspace }}/ws
1818
SRC_DIR: ${{ github.workspace }}/ws/src
19-
IMAGE_URL: https://pub-997cd3005edc4b9db91df913907990bf.r2.dev/autoaim/model_infer_example.jpg
20-
IMAGE_PATH: /tmp/model_infer_example.jpg
19+
TEST_ASSETS_ROOT: /tmp/auto_aim
2120

2221
steps:
2322
- name: Checkout repository into ROS2 workspace
@@ -26,9 +25,16 @@ jobs:
2625
repository: Alliance-Algorithm/rmcs_auto_aim_v2
2726
path: ws/src/rmcs_auto_aim_v2
2827

29-
- name: Download test frame
28+
- name: Install yq dependency
3029
run: |
31-
curl -fsSL "$IMAGE_URL" -o "$IMAGE_PATH"
30+
apt-get update
31+
apt-get install -y yq
32+
33+
- name: Download test assets
34+
shell: bash
35+
run: |
36+
cd "$SRC_DIR/rmcs_auto_aim_v2/test"
37+
TEST_ASSETS_ROOT="$TEST_ASSETS_ROOT" ./download_assets.sh
3238
3339
- name: Configure test project
3440
shell: bash
@@ -49,4 +55,4 @@ jobs:
4955
run: |
5056
source /opt/ros/jazzy/setup.bash
5157
cd "$SRC_DIR/rmcs_auto_aim_v2/test"
52-
IMAGE="$IMAGE_PATH" ctest --test-dir build --output-on-failure
58+
ctest --test-dir build --output-on-failure

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,4 @@ auto use_object(Object& object) -> void {
250250

251251
### 4. 自动化与测试:
252252

253-
一个工程化项目,其测试代码应该占据**一半左右**的代码量,特别是对于 RM 这种对代码稳定性有高要求的场景,同时 `CI/CD` 的妥善使用,可以大大降低我们在更新,部署等场景所花费的精力
253+
一个工程化项目,其测试代码应该占据**一半左右**的代码量,特别是对于 RM 这种对代码稳定性有高要求的场景,同时 `CI/CD` 的妥善使用,可以大大降低我们在更新,部署等场景所花费的精力

config/config.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ capturer:
55
show_loss_framerate: false
66
show_loss_framerate_interval: 500
77
reconnect_wait_interval: 100
8-
source: "hikcamera"
8+
source: "local_video"
99
hikcamera:
1010
# int
1111
timeout_ms: 500
1212
# float
13-
exposure_us: 3000.0
13+
exposure_us: 2000.0
1414
# float
15-
framerate: 120
15+
framerate: 60
1616
# float
1717
gain: 16.9807
1818

@@ -21,8 +21,14 @@ capturer:
2121
trigger_mode: false
2222
fixed_framerate: true
2323
local_video:
24-
location: ""
24+
# 替换为你具体的路径
25+
location: "/workspaces/alliance/test_videos/solve_pnp_v2.mp4"
26+
# double 帧率
27+
frame_rate: 60
28+
# bool 是否循环播放
2529
loop_play: true
30+
# bool 是否允许跳帧以满足实时性
31+
allow_skipping: false
2632

2733
identifier:
2834
binarization_threshold: 0.5
@@ -41,8 +47,8 @@ identifier:
4147
nms_threshold: 0.3
4248

4349
pose_estimator:
44-
camera_matrix: [2414.9359264386621, 0, 717.26243105567414, 0, 2418.0489262208148, 582.68540529942845, 0, 0, 1]
45-
distort_coeff: [-0.0209453389287673, 0.15028138841073832, -0.0006517722113234505, -0.0016861906197686788, 0]
50+
camera_matrix: [1.722231837421459e+03, 0, 7.013056440882832e+02, 0, 1.724876404292754e+03,5.645821718351237e+02 , 0, 0, 1]
51+
distort_coeff: [-0.064232403853946, -0.087667493884102, 0,0, 0.792381808294582]
4652

4753
transforms:
4854
- parent: "imu_link"

package.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
<test_depend>ament_lint_auto</test_depend>
1313
<test_depend>ament_lint_common</test_depend>
1414

15-
<!-- 唯一添加的依赖,为了保证正常的依赖构建顺序 -->
15+
<!-- 添加的两个依赖,为了保证正常的依赖构建顺序 -->
1616
<depend>rmcs_executor</depend>
17+
<depend>hikcamera</depend>
1718

1819
<export>
1920
<build_type>ament_cmake</build_type>
2021
</export>
21-
</package>
22+
</package>

src/component.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ class AutoAimComponent final : public rmcs_executor::Component {
2121
}
2222

2323
auto update() -> void override {
24+
using namespace rmcs_description;
25+
if (rmcs_tf.ready()) [[likely]] {
26+
auto camera_odom =
27+
fast_tf::lookup_transform<rmcs_description::CameraLink, rmcs_description::OdomImu>(
28+
*rmcs_tf);
29+
30+
control_state.timestamp = Clock::now();
31+
32+
control_state.camera_to_odom_transform.posture = camera_odom.translation();
33+
control_state.camera_to_odom_transform.orientation =
34+
Eigen::Quaterniond(camera_odom.rotation());
35+
36+
//...
37+
}
38+
2439
recv_state();
2540
send_state();
2641
}
@@ -33,6 +48,8 @@ class AutoAimComponent final : public rmcs_executor::Component {
3348
ControlClient::Send shm_send;
3449
ControlClient::Recv shm_recv;
3550

51+
ControlState control_state;
52+
3653
FramerateCounter framerate;
3754

3855
private:
@@ -46,6 +63,7 @@ class AutoAimComponent final : public rmcs_executor::Component {
4663

4764
if (shm_recv.is_updated()) {
4865
auto timestamp = Stamp {};
66+
4967
shm_recv.with_read([&](const auto& state) { timestamp = state.timestamp; });
5068

5169
if (shm_recv.is_updated()) {
@@ -67,8 +85,9 @@ class AutoAimComponent final : public rmcs_executor::Component {
6785
return;
6886
}
6987

70-
shm_send.with_write([](ControlState& state) {
71-
state.timestamp = Clock::now();
88+
shm_send.with_write([&](ControlState& state) {
89+
state = control_state;
90+
7291
// ...
7392
});
7493
}

src/kernel/pose_estimator.cpp

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#include "pose_estimator.hpp"
2+
23
#include "kernel/transform_tree.hpp"
34
#include "utility/logging/printer.hpp"
4-
#include "utility/math/solve_pnp.hpp"
5+
#include "utility/math/solve_pnp/pnp_solution.hpp"
6+
#include "utility/math/solve_pnp/solve_pnp.hpp"
57
#include "utility/serializable.hpp"
68
#include "utility/yaml/tf.hpp"
79

@@ -16,25 +18,24 @@ struct PoseEstimator::Impl {
1618
std::array<float, 9> camera_matrix;
1719
std::array<float, 5> distort_coeff;
1820

19-
// clang-format off
2021
constexpr static std::tuple metas {
21-
&Config::camera_matrix, "camera_matrix",
22-
&Config::distort_coeff, "distort_coeff",
22+
&Config::camera_matrix,
23+
"camera_matrix",
24+
&Config::distort_coeff,
25+
"distort_coeff",
2326
};
24-
// clang-format on
2527
};
28+
2629
Config config;
30+
PnpSolution pnp_solution {};
2731

2832
Printer log { "PoseEstimator" };
2933

30-
PnpSolution pnp_solution;
31-
3234
auto initialize(const YAML::Node& yaml) noexcept -> std::expected<void, std::string> try {
3335
auto result = config.serialize(yaml);
3436
if (!result.has_value()) {
3537
return std::unexpected { result.error() };
3638
}
37-
3839
{
3940
auto result = serialize_from<tf::AutoAim>(yaml["transforms"]);
4041
if (!result.has_value()
@@ -43,20 +44,71 @@ struct PoseEstimator::Impl {
4344
+ util::to_string(result.error()) };
4445
}
4546
}
47+
{
48+
pnp_solution.input.camera_matrix =
49+
reshape_array<float, 9, double, 3, 3>(config.camera_matrix);
50+
pnp_solution.input.distort_coeff =
51+
reshape_array<float, 5, double>(config.distort_coeff);
52+
}
4653

4754
return {};
4855
} catch (const std::exception& e) {
4956
return std::unexpected { e.what() };
5057
}
5158

52-
auto transform() { }
59+
auto solve_pnp(std::vector<Armor2D> const& armors) -> std::optional<std::vector<Armor3D>> {
60+
if (armors.empty()) return std::nullopt;
61+
62+
auto armor_shape = [](ArmorShape shape) {
63+
if (shape == ArmorShape::SMALL) {
64+
return rmcs::kSmallArmorShapeOpenCV;
65+
} else {
66+
return rmcs::kLargeArmorShapeOpenCV;
67+
}
68+
};
69+
70+
auto armors_in_camera = std::vector<Armor3D> {};
71+
72+
std::ranges::for_each(armors | std::views::enumerate,
73+
[&armors_in_camera, &armor_shape, this](auto const& item) {
74+
auto [i, armor] = item;
75+
76+
pnp_solution.input.armor_shape = armor_shape(armor.shape);
77+
pnp_solution.input.genre = armor.genre;
78+
pnp_solution.input.color = armor_color2camp_color(armor.color);
79+
std::ranges::copy(armor.corners(), pnp_solution.input.armor_detection.begin());
80+
81+
auto solved = pnp_solution.solve();
82+
if (!solved) {
83+
log.warn("solvePnP failed for armor {} ({} {})", i, get_enum_name(armor.genre),
84+
get_enum_name(armor.color));
85+
return;
86+
}
87+
88+
auto armor_3d = Armor3D {};
89+
armor_3d.genre = pnp_solution.result.genre;
90+
armor_3d.color = camp_color2armor_color(pnp_solution.result.color);
91+
armor_3d.id = i;
92+
pnp_solution.result.translation.copy_to(armor_3d.translation);
93+
pnp_solution.result.orientation.copy_to(armor_3d.orientation);
94+
95+
armors_in_camera.emplace_back(armor_3d);
96+
});
97+
98+
return armors_in_camera;
99+
}
53100
};
54101

55102
auto PoseEstimator::initialize(const YAML::Node& yaml) noexcept
56103
-> std::expected<void, std::string> {
57104
return pimpl->initialize(yaml);
58105
}
59106

107+
auto PoseEstimator::solve_pnp(std::vector<Armor2D> const& armors) const
108+
-> std::optional<std::vector<Armor3D>> {
109+
return pimpl->solve_pnp(armors);
110+
}
111+
60112
PoseEstimator::PoseEstimator() noexcept
61113
: pimpl { std::make_unique<Impl>() } { }
62114

src/kernel/pose_estimator.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#pragma once
2+
23
#include "utility/math/linear.hpp"
34
#include "utility/pimpl.hpp"
5+
#include "utility/rclcpp/node.hpp"
6+
#include "utility/robot/armor.hpp"
47
#include <expected>
58
#include <yaml-cpp/yaml.h>
69

@@ -10,8 +13,14 @@ class PoseEstimator {
1013
RMCS_PIMPL_DEFINITION(PoseEstimator)
1114

1215
public:
16+
using RclcppNode = util::RclcppNode;
17+
1318
auto initialize(const YAML::Node&) noexcept -> std::expected<void, std::string>;
1419

20+
auto visualize(RclcppNode& visual_node) -> void;
21+
22+
auto solve_pnp(std::vector<Armor2D> const&) const -> std::optional<std::vector<Armor3D>>;
23+
1524
auto update_imu_link(const Orientation&) noexcept -> void;
1625
};
1726

src/kernel/visualization.cpp

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#include "visualization.hpp"
2-
#include "module/debug/visualization/stream_session.hpp"
32

3+
#include <fstream>
4+
5+
#include "module/debug/visualization/armor_visualizer.hpp"
6+
#include "module/debug/visualization/stream_session.hpp"
47
#include "utility/image/image.details.hpp"
58
#include "utility/logging/printer.hpp"
69
#include "utility/serializable.hpp"
710

8-
#include <fstream>
9-
1011
using namespace rmcs::kernel;
12+
using namespace rmcs::util;
1113

1214
constexpr std::array kVideoTypes {
1315
"RTP_JEPG",
@@ -47,9 +49,14 @@ struct Visualization::Impl {
4749
bool is_initialized = false;
4850
bool size_determined = false;
4951

50-
Impl() noexcept { session = std::make_unique<debug::StreamSession>(); }
52+
std::unique_ptr<debug::ArmorVisualizer> armor_visualizer;
53+
54+
Impl() noexcept {
55+
session = std::make_unique<debug::StreamSession>();
56+
armor_visualizer = std::make_unique<debug::ArmorVisualizer>();
57+
}
5158

52-
auto initialize(const YAML::Node& yaml) noexcept -> NormalResult {
59+
auto initialize(const YAML::Node& yaml, RclcppNode& visual_node) noexcept -> NormalResult {
5360
auto config = Config {};
5461
auto result = config.serialize(yaml);
5562
if (!result.has_value()) {
@@ -67,6 +74,9 @@ struct Visualization::Impl {
6774
} else {
6875
return std::unexpected { "Unknown video type: " + config.stream_type };
6976
}
77+
78+
armor_visualizer->initialize(visual_node);
79+
7080
is_initialized = true;
7181
return {};
7282
}
@@ -114,11 +124,15 @@ struct Visualization::Impl {
114124

115125
return session->push_frame(mat);
116126
}
127+
auto visualize_armors(std::span<Armor3D> const& armors) const -> bool {
128+
if (!is_initialized) return false;
129+
return armor_visualizer->visualize(armors);
130+
}
117131
};
118132

119-
auto Visualization::initialize(const YAML::Node& yaml) noexcept
133+
auto Visualization::initialize(const YAML::Node& yaml, RclcppNode& visual_node) noexcept
120134
-> std::expected<void, std::string> {
121-
return pimpl->initialize(yaml);
135+
return pimpl->initialize(yaml, visual_node);
122136
}
123137

124138
auto Visualization::initialized() const noexcept -> bool { return pimpl->initialized(); }
@@ -127,6 +141,10 @@ auto Visualization::send_image(const Image& image) noexcept -> bool {
127141
return pimpl->send_image(image);
128142
}
129143

144+
auto Visualization::visualize_armors(std::span<Armor3D> const& armors) const -> bool {
145+
return pimpl->visualize_armors(armors);
146+
}
147+
130148
Visualization::Visualization() noexcept
131149
: pimpl { std::make_unique<Impl>() } { }
132150

src/kernel/visualization.hpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#pragma once
22
#include "utility/image/image.hpp"
3+
#include "utility/rclcpp/node.hpp"
4+
#include "utility/robot/armor.hpp"
35

46
#include <expected>
57
#include <yaml-cpp/yaml.h>
@@ -17,11 +19,14 @@ class Visualization {
1719
}
1820

1921
public:
20-
auto initialize(const YAML::Node&) noexcept -> std::expected<void, std::string>;
22+
auto initialize(const YAML::Node& yaml, util::RclcppNode& visual_node) noexcept
23+
-> std::expected<void, std::string>;
2124

2225
auto initialized() const noexcept -> bool;
2326

2427
auto send_image(const Image&) noexcept -> bool;
28+
29+
auto visualize_armors(std::span<Armor3D> const& armors) const -> bool;
2530
};
2631

2732
}

0 commit comments

Comments
 (0)