Skip to content

Commit ac1a3a7

Browse files
committed
progress modernizing mrpt_nav
1 parent 5efbb05 commit ac1a3a7

File tree

14 files changed

+301
-175
lines changed

14 files changed

+301
-175
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ doc/venv
5151
.cache
5252
my_config.ini
5353
*.ini
54+
cache_*.bin.gz

apps/mrpt_apps_gui/ReactiveNav3D-Demo/ReactiveNav3D_demo_main.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ int main(int num_arg, char* argv[])
124124
ReactInterface.robotSim.simulateOneTimeStep(reactive_period.Tac());
125125
reactive_period.Tic();
126126

127-
if ((rn3d.IDLE == rn3d.getCurrentState()) || (rn3d.SUSPENDED == rn3d.getCurrentState()))
127+
using TState = mrpt::nav::CAbstractNavigator::TState;
128+
if ((rn3d.getCurrentState() == TState::IDLE) || (rn3d.getCurrentState() == TState::SUSPENDED))
128129
{
129130
CSimplePointsMap auxpoints;
130131
mrpt::system::TTimeStamp auxpoints_time;

doc/source/doxygen-docs/lib_mrpt_nav.md

Lines changed: 140 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,145 @@ This library is part of MRPT and can be installed in Debian-based systems with:
1212

1313
Read also [how to import MRPT into your CMake scripts](mrpt_from_cmake.html).
1414

15-
This library implements:
16-
17-
- Reactive navigation:
18-
- Holonomic navigation algorithms: Virtual Force Fields (VFF), Nearness
19-
Diagram (ND), ... See mrpt::nav::CAbstractHolonomicReactiveMethod
20-
- A complex reactive navigator: Using space transformations (PTGs) to drive a
21-
robot using an internal simpler holonomic algorithm. See
22-
mrpt::nav::CReactiveNavigationSystem
23-
- A number of different PTGs: See mrpt::nav::CParameterizedTrajectoryGenerator
24-
- See the full list of classes in mrpt::nav, or the online page
25-
https://www.mrpt.org/list-of-mrpt-apps/application-reactivenavigationdemo/ for a
26-
working application (see MRPT/apps/ReactiveNavigationDemo).
27-
28-
* Planned / hybrid navigation:
29-
* See mrpt::nav
15+
---
16+
17+
## Overview
18+
19+
`mrpt-nav` provides reactive and planned navigation for mobile robots.
20+
The architecture is layered:
21+
22+
```
23+
CAbstractNavigator ← base state machine (IDLE / NAVIGATING / SUSPENDED / NAV_ERROR)
24+
└── CWaypointsNavigator ← waypoint sequencing on top of single-goal navigation
25+
└── CAbstractPTGBasedReactive ← TP-Space reactive core
26+
├── CReactiveNavigationSystem ← 2-D robots
27+
└── CReactiveNavigationSystem3D ← multi-level 3-D robots
28+
```
29+
30+
---
31+
32+
## Reactive navigation
33+
34+
### Navigator state machine
35+
36+
mrpt::nav::CAbstractNavigator drives a state machine with four states
37+
(mrpt::nav::CAbstractNavigator::TState):
38+
39+
| State | Meaning |
40+
|-------|---------|
41+
| `TState::IDLE` | No active navigation goal |
42+
| `TState::NAVIGATING` | Actively navigating toward target |
43+
| `TState::SUSPENDED` | Navigation paused (call `resume()` to continue) |
44+
| `TState::NAV_ERROR` | An unrecoverable error occurred; query `getErrorReason()` for details |
45+
46+
Error codes (mrpt::nav::CAbstractNavigator::TErrorCode) are stored in the
47+
`TErrorReason` struct returned by `getErrorReason()`:
48+
49+
| Code | Meaning |
50+
|------|---------|
51+
| `TErrorCode::NONE` | No error |
52+
| `TErrorCode::EMERGENCY_STOP` | Robot stopped due to safety violation |
53+
| `TErrorCode::CANNOT_REACH_TARGET` | Timeout: robot not approaching target |
54+
| `TErrorCode::OTHER` | Unclassified exception |
55+
56+
### Waypoint navigation
57+
58+
mrpt::nav::CWaypointsNavigator extends the base navigator with a sequence of
59+
waypoints (`TWaypointSequence`). Each waypoint can optionally be skipped if the
60+
robot can reach a later one more directly.
61+
62+
Thread-safe access to the waypoint list:
63+
64+
```cpp
65+
// Preferred RAII form:
66+
auto guard = nav.getWaypointsAccessGuard();
67+
guard.waypoints().waypoints[2].allow_skip = false;
68+
// mutex released automatically when guard goes out of scope
69+
```
70+
71+
### TP-Space reactive core
72+
73+
mrpt::nav::CAbstractPTGBasedReactive implements the TP-Space obstacle
74+
transformation method:
75+
76+
1. For each PTG (Parameterized Trajectory Generator), obstacles in workspace
77+
are projected into TP-Space via `CParameterizedTrajectoryGenerator::updateTPObstacle()`.
78+
2. The selected holonomic method (`CHolonomicVFF`, `CHolonomicND`, or
79+
`CHolonomicFullEval`) picks a direction in the normalized TP-Space.
80+
3. The chosen TP-Space motion is mapped back to a velocity command via
81+
`CParameterizedTrajectoryGenerator::directionToMotionCommand()`.
82+
83+
---
84+
85+
## Holonomic navigation methods
86+
87+
All holonomic methods derive from mrpt::nav::CAbstractHolonomicReactiveMethod
88+
and implement `navigate(NavInput, NavOutput)`.
89+
90+
| Class | Algorithm |
91+
|-------|-----------|
92+
| mrpt::nav::CHolonomicVFF | Virtual Force Fields — repulsive forces from obstacles + attractive force toward target |
93+
| mrpt::nav::CHolonomicND | Nearness Diagram — gap-based obstacle avoidance (Minguez & Montano, 2004) |
94+
| mrpt::nav::CHolonomicFullEval | Full-evaluation scoring across all TP-Space directions |
95+
96+
The navigation situation selected by `CHolonomicND` is recorded in
97+
`CLogFileRecord_ND::situation` (mrpt::nav::CHolonomicND::TSituations):
98+
99+
| Value | Meaning |
100+
|-------|---------|
101+
| `TSituations::TARGET_DIRECTLY` | Straight free path to target |
102+
| `TSituations::SMALL_GAP` | Narrow gap selected |
103+
| `TSituations::WIDE_GAP` | Wide gap selected |
104+
| `TSituations::NO_WAY_FOUND` | No traversable gap; robot stops |
105+
106+
---
107+
108+
## Parameterized Trajectory Generators (PTGs)
109+
110+
PTGs define families of robot trajectories parameterized by a heading angle α.
111+
They transform between Workspace (WS) and TP-Space. All derive from
112+
mrpt::nav::CParameterizedTrajectoryGenerator.
113+
114+
| Class | Robot kinematics |
115+
|-------|-----------------|
116+
| `CPTG_DiffDrive_C` | Differential drive — circular arc |
117+
| `CPTG_DiffDrive_CS` | Differential drive — circular arc + straight |
118+
| `CPTG_DiffDrive_CC` | Differential drive — two circular arcs (same direction) |
119+
| `CPTG_DiffDrive_CCS` | Differential drive — two arcs + straight |
120+
| `CPTG_DiffDrive_alpha` | Differential drive — trapezoidal steering |
121+
| `CPTG_Holo_Blend` | Holonomic robot with velocity blending |
122+
123+
Collision behavior when an obstacle is detected *inside* the robot shape at the
124+
start of a PTG path is controlled globally via
125+
`CParameterizedTrajectoryGenerator::COLLISION_BEHAVIOR()`
126+
(mrpt::nav::PTGCollisionBehavior):
127+
128+
| Value | Effect |
129+
|-------|--------|
130+
| `PTGCollisionBehavior::BACK_AWAY` | (default) Allow reverse motions to escape near-collision |
131+
| `PTGCollisionBehavior::STOP` | Reject any motion when robot is already in near-collision |
132+
133+
---
134+
135+
## Path planning
136+
137+
| Class | Algorithm |
138+
|-------|-----------|
139+
| mrpt::nav::PlannerSimple2D | Simple 2-D A\* on an occupancy grid |
140+
| mrpt::nav::PlannerRRT_SE2_TPS | RRT planner in SE(2) using PTG-space expansion |
141+
142+
---
143+
144+
## Robot interface
145+
146+
Users must implement mrpt::nav::CRobot2NavInterface, providing:
147+
- `getCurrentPoseAndSpeeds()` — current robot pose and velocity
148+
- `changeSpeed()` / `stop()` — velocity commands
149+
- Event callbacks: `sendNavigationStartEvent()`, `sendNavigationEndEvent()`, etc.
150+
151+
mrpt::nav::CRobot2NavInterfaceForSimulator offers a ready-made implementation
152+
backed by a kinematic simulator.
153+
154+
---
30155

31156
# Library contents

doc/source/doxygen-docs/port_mrpt3.md

Lines changed: 57 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -327,32 +327,6 @@ run-time `THROW_EXCEPTION("TODO")`, carry `MRPT_TODO` or `// TODO` markers, or
327327
are otherwise visibly incomplete after the 2.x → 3.0 porting effort.
328328
329329
330-
### 13.4 `mrpt_poses` — Unimplemented PDF operations
331-
332-
All previously-stubbed methods have been implemented:
333-
334-
- **`CPosePDFGrid`**: `copyFrom()` (same-type copy or sample-based approximation),
335-
`changeCoordinatesReference()` (cell remapping with bounds check),
336-
`bayesianFusion()` (element-wise product for same-type grids, or Parzen KDE
337-
for particle inputs), `inverse()` (cell remapping via SE(2) inverse).
338-
- **`CPose3DPDFGrid`**: `copyFrom()`, `saveToTextFile()` (sparse text dump with
339-
header), `changeCoordinatesReference()`, `bayesianFusion()` (same-size grids),
340-
`inverse()`.
341-
- **`CPose3DPDFParticles`**: `bayesianFusion()` — KDE-based fusion when p2 is
342-
particles, Mahalanobis-based when p2 is Gaussian.
343-
- **`CPosePDFParticles`**: `bayesianFusion()` — Parzen window evaluation via the
344-
existing `evaluatePDF_parzen()` helper; Mahalanobis fallback for other types.
345-
- **`CPointPDFParticles`**: `copyFrom()` (same-type deep copy, or sampling from
346-
Gaussian), `bayesianFusion()` — KDE-based particle fusion.
347-
348-
### 13.5 `mrpt_slam` — Incomplete algorithms
349-
350-
- **`CICP` (3-D mode)**: Only `icpClassic` is implemented for 3-D point-cloud
351-
alignment. The `CICP` class Doxygen header now documents this limitation and
352-
directs users to the **mp2p_icp** library
353-
(https://github.com/MOLAorg/mp2p_icp) for production-quality 3-D scan
354-
matching with richer metrics and robust kernels.
355-
356330
### 13.6 `mrpt_nav` — Comprehensive modernisation plan
357331
358332
The `mrpt_nav` module is ~20 years old and the largest candidate for
@@ -366,110 +340,90 @@ increasing risk/effort. Each item is prefixed with a priority tag:
366340
367341
#### 13.6.1 Enum modernisation
368342
369-
**[P0]** Convert all plain `enum` to `enum class`:
343+
**[DONE]** Converted all plain `enum` to `enum class`, with redundant prefixes removed from values:
370344
371-
| Current | Proposed |
372-
|---------|----------|
373-
| `CAbstractNavigator::TState` (`IDLE`, `NAVIGATING`, …) | `enum class TState` |
374-
| `CAbstractNavigator::TErrorCode` (`ERR_NONE`, …) | `enum class TErrorCode` |
375-
| `CHolonomicND::TSituations` (`SITUATION_TARGET_DIRECTLY`, …) | `enum class TSituations` |
376-
| `PTG_collision_behavior_t` (`COLL_BEH_BACK_AWAY`, …) | `enum class PTGCollisionBehavior` (rename too) |
345+
| Old | New |
346+
|-----|-----|
347+
| `CAbstractNavigator::TState` `IDLE`, `NAVIGATING`, `SUSPENDED`, `NAV_ERROR` | `enum class TState` — same values (`NAV_ERROR` kept to avoid clash with Windows `ERROR` macro) |
348+
| `CAbstractNavigator::TErrorCode` `ERR_NONE`, `ERR_EMERGENCY_STOP`, `ERR_CANNOT_REACH_TARGET`, `ERR_OTHER` | `enum class TErrorCode` — `NONE`, `EMERGENCY_STOP`, `CANNOT_REACH_TARGET`, `OTHER` |
349+
| `CHolonomicND::TSituations` `SITUATION_TARGET_DIRECTLY`, `SITUATION_SMALL_GAP`, `SITUATION_WIDE_GAP`, `SITUATION_NO_WAY_FOUND` | `enum class TSituations` — `TARGET_DIRECTLY`, `SMALL_GAP`, `WIDE_GAP`, `NO_WAY_FOUND` |
350+
| `PTG_collision_behavior_t` `COLL_BEH_BACK_AWAY`, `COLL_BEH_STOP` | `enum class PTGCollisionBehavior` — `BACK_AWAY`, `STOP` |
377351
378352
---
379353
380354
#### 13.6.2 `[[nodiscard]]` annotations
381355
382-
**[P0]** Add `[[nodiscard]]` to every query / getter that returns a
383-
value callers should not silently discard:
356+
**[DONE]** Added `[[nodiscard]]` to:
357+
358+
- `CAbstractNavigator`: `getCurrentState()`, `getErrorReason()`,
359+
`getFrameTF()`, `isRethrowNavExceptionsEnabled()`, `getDelaysTimeLogger()`
360+
- `CParameterizedTrajectoryGenerator`: `getDescription()`, `supportVelCmdNOP()`,
361+
`supportSpeedAtTarget()`, `isInitialized()`, `getAlphaValuesCount()`,
362+
`getPathCount()`, `getRefDistance()`, `getScorePriority()`,
363+
`getClearanceStepCount()`, `getClearanceDecimatedPaths()`, `getPathStepCount()`,
364+
`getPathPose()`, `getPathDist()`, `getPathStepDuration()`, `getMaxLinVel()`,
365+
`getMaxAngVel()`, `getMaxRobotRadius()`, `isPointInsideRobotShape()`
366+
- `CAbstractHolonomicReactiveMethod`: `getAssociatedPTG()`
367+
- `CWaypointsNavigator`: `getWaypointsAccessGuard()`
384368
385-
- `CAbstractNavigator`: `getCurrentState()`, `getErrorReason()`
386-
- `CWaypointsNavigator`: `getWaypointNavStatus()`
387-
- `CAbstractPTGBasedReactive`: `getPTG_count()`, `getPTG()`,
388-
`getLastLogRecord()`, `getTargetApproachSlowDownDistance()`
389-
- `CParameterizedTrajectoryGenerator`: `getRefDistance()`,
390-
`getAlphaValuesCount()`, `getPathStepCount()`, `getPathPose()`,
391-
`getPathDist()`, `index2alpha()`, `alpha2index()`, `isInitialized()`
392-
- All holonomic `TOptions` getters.
369+
Still pending: `CAbstractPTGBasedReactive` getters, holonomic `TOptions` getters.
393370
394371
---
395372
396373
#### 13.6.3 Naming consistency
397374
398-
**[P0]** Standardise naming across the module:
399-
400-
- **Config structs**: Rename all `TOptions` to `TParams` (or vice-versa)
401-
so that every configuration struct in the module uses the same suffix.
402-
Holonomic classes use `TOptions`; navigator classes use `TParams`;
403-
`CMultiObjectiveMotionOptimizerBase` uses `TParamsBase`. Pick one.
404-
- **STEP-numbered methods**: `STEP1_InitPTGs()`, `STEP2_SenseObstacles()`,
405-
`STEP3_WSpaceToTPSpace()`, `STEP8_GenerateLogRecord()` should be renamed
406-
to descriptive names (e.g. `initPTGs()`, `senseObstacles()`,
407-
`workspaceToTPSpace()`, `generateLogRecord()`).
408-
- **`impl_` prefix**: Some virtual hooks use `impl_` (e.g.
409-
`impl_waypoint_is_reachable`), others do not. Standardise on a single
410-
convention (recommend dropping the prefix and relying on `protected` access).
411-
- **Underscore style in identifiers**: `TP_Obstacles` vs `TPObstacles`,
412-
`WS_Obstacles` vs `PWS_Obstacles`. Pick one convention.
413-
- **`getDescription()` in PTGs**: The `os::sprintf` calls in
414-
`CPTG_DiffDrive_CC`, `CPTG_DiffDrive_CCS`, `CPTG_DiffDrive_CS`,
415-
`CPTG_DiffDrive_alpha` should use `mrpt::format()` and their C-style
416-
casts `(int)K` should become `static_cast<int>(K)`.
375+
**[PARTIAL]** Done:
376+
- `CPTG_DiffDrive_CC/CCS/alpha`: replaced `os::sprintf` + `(int)K` casts with
377+
`mrpt::format()` + `static_cast<int>(K)`.
378+
379+
Remaining:
380+
- **Config structs**: Holonomic classes use `TOptions`; navigator classes use `TParams`.
381+
Pick one suffix uniformly.
382+
- **STEP-numbered methods**: `STEP1_InitPTGs()` … `STEP8_GenerateLogRecord()` —
383+
rename to descriptive names.
384+
- **`impl_` prefix**: Standardise virtual hook naming convention.
417385
418386
---
419387
420388
#### 13.6.4 Thread-safety fixes
421389
422-
**[P1]** Eliminate bare `lock()`/`unlock()` calls and manual locking APIs:
423-
424-
- `CAbstractPTGBasedReactive::preDestructor()` (line 75–76) does
425-
`m_nav_cs.lock(); m_nav_cs.unlock();` outside any RAII guard — an
426-
exception between these calls would deadlock. Replace with
427-
`std::scoped_lock lk(m_nav_cs);`.
428-
- `CWaypointsNavigator::beginWaypointsAccess()` /
429-
`endWaypointsAccess()` expose raw `lock()`/`unlock()` to users.
430-
Replace with a RAII accessor that returns a guard object holding a
431-
reference to the waypoint data:
432-
```cpp
433-
[[nodiscard]] auto waypointsAccess() {
434-
return mrpt::lockHelper(m_nav_waypoints_cs, m_waypoints);
435-
}
436-
```
437-
- **Global mutable state**: `OUTPUT_DEBUG_PATH_PREFIX` and
438-
`COLLISION_BEHAVIOR` in `CParameterizedTrajectoryGenerator.cpp` are
439-
`static` globals with no synchronisation. Guard with
440-
`std::mutex` or make them `thread_local`, or move them into a
441-
per-instance configuration field.
390+
**[DONE]**:
391+
- `CAbstractPTGBasedReactive::preDestructor()`: replaced bare
392+
`m_nav_cs.lock(); m_nav_cs.unlock()` with `{ std::scoped_lock lck(m_nav_cs); }`.
393+
- `CWaypointsNavigator`: replaced `beginWaypointsAccess()` / `endWaypointsAccess()`
394+
with `getWaypointsAccessGuard()` returning a `WaypointsAccessGuard` RAII object
395+
that holds `std::unique_lock<std::recursive_mutex>` and exposes `waypoints()`.
396+
397+
Remaining: global mutable `OUTPUT_DEBUG_PATH_PREFIX` / `COLLISION_BEHAVIOR` statics
398+
have no synchronisation — consider moving to per-instance fields or guarding with mutex.
442399
443400
---
444401
445402
#### 13.6.5 Return-value modernisation (output-reference cleanup)
446403
447-
**[P1]** Replace output-reference parameters with return values or
448-
structured returns. Key candidates:
404+
**[TODO — P1]** Key candidates:
449405
450406
| Method | Current | Proposed |
451407
|--------|---------|----------|
452408
| `CRobot2NavInterface::getCurrentPoseAndSpeeds()` | 4 output `&` params | Return `struct PoseAndSpeeds { TPose2D pose; TTwist2D vel; … };` |
453409
| `CAbstractHolonomicReactiveMethod::navigate()` | `NavOutput&` | Return `NavOutput` by value |
454-
| `CAbstractPTGBasedReactive::getLastLogRecord()` | `CLogFileRecord&` output | Return `std::optional<CLogFileRecord>` or `CLogFileRecord` |
455410
| `CMultiObjectiveMotionOptimizerBase::decide()` | `int&` best index | Return `std::optional<size_t>` (nullopt = no good motion) |
456411
| `CParameterizedTrajectoryGenerator::inverseMap_WS2TP()` | `int& k, double& d` output | Return `std::optional<std::pair<uint16_t,double>>` |
457412
458413
---
459414
460415
#### 13.6.6 Raw-pointer cleanup
461416
462-
**[P1]** Remove remaining raw-pointer ownership/borrowing:
417+
**[DONE — documented]**: Both `m_associatedPTG` in `CAbstractHolonomicReactiveMethod`
418+
and `TCandidateMovementPTG::PTG` are non-owning observer raw pointers. Both are now
419+
documented explicitly as such, with lifetime contract stated. Since `nullptr` is a valid
420+
state and the PTG lifetime is guaranteed by the owning reactive system, a raw pointer
421+
is the correct tool; `std::weak_ptr` would require the entire PTG ownership chain to
422+
be changed to shared ownership.
463423
464-
- `CAbstractHolonomicReactiveMethod::m_associatedPTG` — raw `const
465-
CParameterizedTrajectoryGenerator*`. Replace with
466-
`std::weak_ptr<const CParameterizedTrajectoryGenerator>` or a
467-
non-owning `std::reference_wrapper`.
468-
- `TCandidateMovementPTG::PTG` — raw pointer, nullable. Replace with
469-
`const CParameterizedTrajectoryGenerator*` documented as non-owning,
470-
or better, an index into the PTG vector.
471-
- `CAbstractPTGBasedReactive::getHoloMethod()` returns a raw pointer;
472-
return a reference or `std::optional<std::reference_wrapper<>>`.
424+
Remaining: `CAbstractPTGBasedReactive::getHoloMethod()` returns a raw pointer;
425+
consider returning `CAbstractHolonomicReactiveMethod&` (with assert) for non-nullable
426+
use sites.
473427
474428
---
475429
@@ -510,14 +464,14 @@ abstraction. Each should be broken into named helpers:
510464
511465
#### 13.6.9 Magic numbers → named constants
512466
513-
**[P1]** Replace hard-coded thresholds with named constants or
514-
configuration-file parameters:
467+
**[PARTIAL]** Done:
468+
- `CHolonomicVFF.cpp`: `1e6` → `MAX_FORCE_CAP`, `20.0` → `OBSTACLE_WEIGHT_FACTOR`,
469+
`6.0` → `OBSTACLE_NEARNESS_THRESHOLD`.
470+
- `CHolonomicND.cpp`: `0.02` → `DIRECT_PATH_SECTOR_FRACTION`,
471+
`1.05` → `DIRECT_PATH_DIST_MARGIN`, `0.95` → `DIRECT_PATH_RANGE_FRACTION`,
472+
`0.01` → `MIN_TARGET_DIST`.
515473
516-
- `CHolonomicFullEval.cpp`: 0.01, 0.95, 1.02, 1.05, `round(nDirs * 0.1)`
517-
- `CHolonomicND.cpp`: 0.01, 0.05, 0.02, 0.1, 0.5 (ratio thresholds)
518-
- `CHolonomicVFF.cpp`: `std::min(1e6, …)` sentinel
519-
- `PlannerSimple2D.cpp`: `CELL_EMPTY = 0x8000000` etc. — already named,
520-
but could use documentation comments explaining why those values.
474+
Remaining: `CHolonomicFullEval.cpp` thresholds (0.01, 0.95, 1.02, 1.05, etc.).
521475
522476
---
523477
@@ -598,7 +552,7 @@ minimal stubs:
598552
| Page | Issue |
599553
|------|-------|
600554
| `lib_mrpt_slam.md` | References `mrpt-slam` (hyphen) in text; could expand on available algorithms |
601-
| `lib_mrpt_nav.md` | Stubminimal description of reactive navigation |
555+
| `lib_mrpt_nav.md` | **[DONE]**expanded with architecture diagram, state machine tables, PTG/holonomic class tables, waypoint API, robot interface guide |
602556
| `lib_mrpt_hwdrivers.md` | Stub — no list of supported sensor classes |
603557
| `lib_mrpt_core.md` | Stub — needs description of core utilities |
604558
| `lib_mrpt_poses.md` | Stub — needs overview of pose PDF classes |

0 commit comments

Comments
 (0)