1use std::f64::consts::PI;
18
19use rand::Rng;
20
21use rust_robotics_core::types::Pose2D;
22
23use crate::dubins_path::DubinsPlanner;
24use crate::rrt::{AreaBounds, CircleObstacle};
25
26#[derive(Debug, Clone)]
28pub struct RRTDubinsNode {
29 pub x: f64,
30 pub y: f64,
31 pub yaw: f64,
32 pub path_x: Vec<f64>,
33 pub path_y: Vec<f64>,
34 pub path_yaw: Vec<f64>,
35 pub cost: f64,
36 pub parent: Option<usize>,
37}
38
39impl RRTDubinsNode {
40 pub fn new(x: f64, y: f64, yaw: f64) -> Self {
41 Self {
42 x,
43 y,
44 yaw,
45 path_x: Vec::new(),
46 path_y: Vec::new(),
47 path_yaw: Vec::new(),
48 cost: 0.0,
49 parent: None,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct RRTDubinsConfig {
57 pub curvature: f64,
59 pub goal_sample_rate: i32,
61 pub max_iter: usize,
63 pub goal_xy_threshold: f64,
65 pub goal_yaw_threshold: f64,
67 pub robot_radius: f64,
69 pub path_resolution: f64,
71}
72
73impl Default for RRTDubinsConfig {
74 fn default() -> Self {
75 Self {
76 curvature: 1.0,
77 goal_sample_rate: 10,
78 max_iter: 200,
79 goal_xy_threshold: 0.5,
80 goal_yaw_threshold: 1.0_f64.to_radians(),
81 robot_radius: 0.0,
82 path_resolution: 0.1,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct RRTDubinsPath {
90 pub poses: Vec<Pose2D>,
92}
93
94pub struct RRTDubinsPlanner {
96 config: RRTDubinsConfig,
97 obstacles: Vec<CircleObstacle>,
98 rand_area: AreaBounds,
99 dubins: DubinsPlanner,
100 node_list: Vec<RRTDubinsNode>,
101 start: RRTDubinsNode,
102 goal: RRTDubinsNode,
103}
104
105impl RRTDubinsPlanner {
106 pub fn new(
108 obstacles: Vec<CircleObstacle>,
109 rand_area: AreaBounds,
110 config: RRTDubinsConfig,
111 ) -> Self {
112 let dubins = DubinsPlanner::new(config.curvature);
113 Self {
114 config,
115 obstacles,
116 rand_area,
117 dubins,
118 node_list: Vec::new(),
119 start: RRTDubinsNode::new(0.0, 0.0, 0.0),
120 goal: RRTDubinsNode::new(0.0, 0.0, 0.0),
121 }
122 }
123
124 pub fn planning(&mut self, start: Pose2D, goal: Pose2D) -> Option<RRTDubinsPath> {
128 self.start = RRTDubinsNode::new(start.x, start.y, start.yaw);
129 self.goal = RRTDubinsNode::new(goal.x, goal.y, goal.yaw);
130 self.node_list = vec![self.start.clone()];
131
132 for _ in 0..self.config.max_iter {
133 let rnd = self.get_random_node();
134 let nearest_ind = self.get_nearest_node_index(&rnd);
135
136 if let Some(new_node) =
137 self.steer(&self.node_list[nearest_ind].clone(), &rnd, nearest_ind)
138 {
139 if self.check_collision(&new_node) {
140 self.node_list.push(new_node);
141
142 if let Some(goal_idx) = self.search_best_goal_node() {
144 return Some(self.generate_final_course(goal_idx));
145 }
146 }
147 }
148 }
149
150 self.search_best_goal_node()
152 .map(|idx| self.generate_final_course(idx))
153 }
154
155 pub fn plan_with_sampler<F>(
157 &mut self,
158 start: Pose2D,
159 goal: Pose2D,
160 mut sample_node: F,
161 ) -> Option<RRTDubinsPath>
162 where
163 F: FnMut(&Self) -> RRTDubinsNode,
164 {
165 self.start = RRTDubinsNode::new(start.x, start.y, start.yaw);
166 self.goal = RRTDubinsNode::new(goal.x, goal.y, goal.yaw);
167 self.node_list = vec![self.start.clone()];
168
169 for _ in 0..self.config.max_iter {
170 let rnd = sample_node(self);
171 let nearest_ind = self.get_nearest_node_index(&rnd);
172
173 if let Some(new_node) =
174 self.steer(&self.node_list[nearest_ind].clone(), &rnd, nearest_ind)
175 {
176 if self.check_collision(&new_node) {
177 self.node_list.push(new_node);
178 }
179 }
180
181 if let Some(goal_idx) = self.search_best_goal_node() {
182 return Some(self.generate_final_course(goal_idx));
183 }
184 }
185
186 self.search_best_goal_node()
187 .map(|idx| self.generate_final_course(idx))
188 }
189
190 pub fn get_tree(&self) -> &[RRTDubinsNode] {
192 &self.node_list
193 }
194
195 fn get_random_node(&self) -> RRTDubinsNode {
196 let mut rng = rand::rng();
197 if rng.random_range(0..=100) > self.config.goal_sample_rate {
198 RRTDubinsNode::new(
199 rng.random_range(self.rand_area.xmin..=self.rand_area.xmax),
200 rng.random_range(self.rand_area.ymin..=self.rand_area.ymax),
201 rng.random_range(-PI..=PI),
202 )
203 } else {
204 RRTDubinsNode::new(self.goal.x, self.goal.y, self.goal.yaw)
205 }
206 }
207
208 fn get_nearest_node_index(&self, rnd_node: &RRTDubinsNode) -> usize {
209 let mut min_dist = f64::INFINITY;
210 let mut min_ind = 0;
211 for (i, node) in self.node_list.iter().enumerate() {
212 let dist = (node.x - rnd_node.x).powi(2) + (node.y - rnd_node.y).powi(2);
213 if dist < min_dist {
214 min_dist = dist;
215 min_ind = i;
216 }
217 }
218 min_ind
219 }
220
221 fn steer(
228 &self,
229 from_node: &RRTDubinsNode,
230 to_node: &RRTDubinsNode,
231 from_index: usize,
232 ) -> Option<RRTDubinsNode> {
233 let from_pose = Pose2D::new(from_node.x, from_node.y, from_node.yaw);
234 let to_pose = Pose2D::new(to_node.x, to_node.y, to_node.yaw);
235
236 let dubins_path = self.dubins.plan(from_pose, to_pose).ok()?;
237
238 let (px, py, pyaw) = sample_dubins_with_yaw(&dubins_path, self.config.path_resolution);
240
241 if px.len() <= 1 {
242 return None;
243 }
244
245 let mut new_node = from_node.clone();
246 new_node.x = *px.last().unwrap();
247 new_node.y = *py.last().unwrap();
248 new_node.yaw = *pyaw.last().unwrap();
249 new_node.path_x = px;
250 new_node.path_y = py;
251 new_node.path_yaw = pyaw;
252 new_node.cost += dubins_path.total_length;
253 new_node.parent = Some(from_index);
254
255 Some(new_node)
256 }
257
258 fn check_collision(&self, node: &RRTDubinsNode) -> bool {
262 for obs in &self.obstacles {
263 for (&px, &py) in node.path_x.iter().zip(node.path_y.iter()) {
264 let dx = obs.x - px;
265 let dy = obs.y - py;
266 let d = (dx * dx + dy * dy).sqrt();
267 if d <= obs.radius + self.config.robot_radius {
268 return false;
269 }
270 }
271 }
272 true
273 }
274
275 fn search_best_goal_node(&self) -> Option<usize> {
278 let mut candidates: Vec<usize> = Vec::new();
279
280 for (i, node) in self.node_list.iter().enumerate() {
281 let dx = node.x - self.goal.x;
282 let dy = node.y - self.goal.y;
283 let dist = (dx * dx + dy * dy).sqrt();
284 if dist > self.config.goal_xy_threshold {
285 continue;
286 }
287 let dyaw = angle_diff(node.yaw, self.goal.yaw).abs();
288 if dyaw > self.config.goal_yaw_threshold {
289 continue;
290 }
291 candidates.push(i);
292 }
293
294 if candidates.is_empty() {
295 return None;
296 }
297
298 candidates.into_iter().min_by(|&a, &b| {
300 self.node_list[a]
301 .cost
302 .partial_cmp(&self.node_list[b].cost)
303 .unwrap_or(std::cmp::Ordering::Equal)
304 })
305 }
306
307 fn generate_final_course(&self, goal_index: usize) -> RRTDubinsPath {
309 let mut poses: Vec<Pose2D> = vec![Pose2D::new(self.goal.x, self.goal.y, self.goal.yaw)];
310
311 let mut node = &self.node_list[goal_index];
312 while node.parent.is_some() {
313 for ((&px, &py), &pyaw) in node
314 .path_x
315 .iter()
316 .rev()
317 .zip(node.path_y.iter().rev())
318 .zip(node.path_yaw.iter().rev())
319 {
320 poses.push(Pose2D::new(px, py, pyaw));
321 }
322 node = &self.node_list[node.parent.unwrap()];
323 }
324
325 poses.push(Pose2D::new(self.start.x, self.start.y, self.start.yaw));
326 poses.reverse();
327
328 RRTDubinsPath { poses }
329 }
330}
331
332fn angle_diff(a: f64, b: f64) -> f64 {
338 let mut d = a - b;
339 while d > PI {
340 d -= 2.0 * PI;
341 }
342 while d < -PI {
343 d += 2.0 * PI;
344 }
345 d
346}
347
348pub fn sample_dubins_with_yaw(
353 path: &crate::dubins_path::DubinsPath,
354 step: f64,
355) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
356 use crate::dubins_path::SegmentType;
357 use rust_robotics_core::types::Point2D;
358
359 let step = if step <= 0.0 { 0.1 } else { step };
360 let segments = path.path_type.segments();
361
362 let mut xs = vec![path.start.x];
363 let mut ys = vec![path.start.y];
364 let mut yaws = vec![path.start.yaw];
365
366 let mut current_yaw = path.start.yaw;
368
369 for (i, &seg) in segments.iter().enumerate() {
370 let seg_len = path.lengths[i] * path.curvature; if seg_len.abs() < 1e-12 {
372 continue;
373 }
374
375 let origin = Point2D::new(*xs.last().unwrap(), *ys.last().unwrap());
376 let origin_yaw = current_yaw;
377
378 let mut cl = step;
379 while (cl + step).abs() <= seg_len.abs() {
380 let (x, y, yaw) = interpolate_segment(cl, seg, path.curvature, origin, origin_yaw);
381 xs.push(x);
382 ys.push(y);
383 yaws.push(yaw);
384 cl += step;
385 }
386
387 let (x, y, yaw) = interpolate_segment(seg_len, seg, path.curvature, origin, origin_yaw);
389 xs.push(x);
390 ys.push(y);
391 yaws.push(yaw);
392
393 current_yaw = match seg {
395 SegmentType::Straight => current_yaw,
396 SegmentType::Left => current_yaw + seg_len,
397 SegmentType::Right => current_yaw - seg_len,
398 };
399 }
400
401 (xs, ys, yaws)
402}
403
404fn interpolate_segment(
408 length: f64,
409 seg: crate::dubins_path::SegmentType,
410 curvature: f64,
411 origin: rust_robotics_core::types::Point2D,
412 origin_yaw: f64,
413) -> (f64, f64, f64) {
414 use crate::dubins_path::SegmentType;
415
416 match seg {
417 SegmentType::Straight => {
418 let nx = origin.x + length / curvature * origin_yaw.cos();
419 let ny = origin.y + length / curvature * origin_yaw.sin();
420 (nx, ny, origin_yaw)
421 }
422 SegmentType::Left => {
423 let r = 1.0 / curvature;
424 let ldx = length.sin() * r;
425 let ldy = (1.0 - length.cos()) * r;
426 let gdx = origin_yaw.cos() * ldx - origin_yaw.sin() * ldy;
427 let gdy = origin_yaw.sin() * ldx + origin_yaw.cos() * ldy;
428 (origin.x + gdx, origin.y + gdy, origin_yaw + length)
429 }
430 SegmentType::Right => {
431 let r = 1.0 / curvature;
432 let ldx = length.sin() * r;
433 let ldy = -(1.0 - length.cos()) * r;
434 let gdx = origin_yaw.cos() * ldx - origin_yaw.sin() * ldy;
435 let gdy = origin_yaw.sin() * ldx + origin_yaw.cos() * ldy;
436 (origin.x + gdx, origin.y + gdy, origin_yaw - length)
437 }
438 }
439}
440
441#[cfg(test)]
446mod tests {
447 use super::*;
448
449 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
450 (a - b).abs() < tol
451 }
452
453 fn create_obstacle_list() -> Vec<CircleObstacle> {
454 vec![
455 CircleObstacle::new(5.0, 5.0, 1.0),
456 CircleObstacle::new(3.0, 6.0, 2.0),
457 CircleObstacle::new(3.0, 8.0, 2.0),
458 CircleObstacle::new(3.0, 10.0, 2.0),
459 CircleObstacle::new(7.0, 5.0, 2.0),
460 CircleObstacle::new(9.0, 5.0, 2.0),
461 ]
462 }
463
464 #[test]
465 fn test_rrt_dubins_node_creation() {
466 let node = RRTDubinsNode::new(1.0, 2.0, 0.5);
467 assert_eq!(node.x, 1.0);
468 assert_eq!(node.y, 2.0);
469 assert_eq!(node.yaw, 0.5);
470 assert_eq!(node.cost, 0.0);
471 assert!(node.parent.is_none());
472 assert!(node.path_x.is_empty());
473 }
474
475 #[test]
476 fn test_rrt_dubins_config_default() {
477 let config = RRTDubinsConfig::default();
478 assert_eq!(config.curvature, 1.0);
479 assert_eq!(config.max_iter, 200);
480 assert_eq!(config.goal_sample_rate, 10);
481 assert!(approx_eq(config.goal_xy_threshold, 0.5, 1e-12));
482 }
483
484 #[test]
485 fn test_angle_diff() {
486 assert!(approx_eq(angle_diff(0.0, 0.0), 0.0, 1e-12));
487 assert!(approx_eq(angle_diff(PI, 0.0), PI, 1e-12));
488 assert!(approx_eq(angle_diff(0.0, PI), -PI, 1e-12));
489 assert!(approx_eq(angle_diff(3.0 * PI / 2.0, -PI / 2.0), 0.0, 1e-12));
491 }
492
493 #[test]
494 fn test_collision_check_no_obstacles() {
495 let obstacles = vec![];
496 let config = RRTDubinsConfig::default();
497 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
498 let planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
499
500 let mut node = RRTDubinsNode::new(1.0, 1.0, 0.0);
501 node.path_x = vec![0.0, 0.5, 1.0];
502 node.path_y = vec![0.0, 0.5, 1.0];
503 assert!(planner.check_collision(&node));
504 }
505
506 #[test]
507 fn test_collision_check_with_obstacle() {
508 let obstacles = vec![CircleObstacle::new(0.5, 0.5, 0.3)];
509 let config = RRTDubinsConfig::default();
510 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
511 let planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
512
513 let mut node = RRTDubinsNode::new(1.0, 1.0, 0.0);
514 node.path_x = vec![0.0, 0.5, 1.0];
515 node.path_y = vec![0.0, 0.5, 1.0];
516 assert!(!planner.check_collision(&node));
518 }
519
520 #[test]
521 fn test_steer_produces_dubins_path() {
522 let obstacles = vec![];
523 let config = RRTDubinsConfig::default();
524 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
525 let mut planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
526 planner.start = RRTDubinsNode::new(0.0, 0.0, 0.0);
527 planner.goal = RRTDubinsNode::new(10.0, 10.0, 0.0);
528 planner.node_list = vec![planner.start.clone()];
529
530 let from_node = RRTDubinsNode::new(0.0, 0.0, 0.0);
531 let to_node = RRTDubinsNode::new(5.0, 5.0, PI / 2.0);
532
533 let result = planner.steer(&from_node, &to_node, 0);
534 assert!(result.is_some());
535
536 let new_node = result.unwrap();
537 assert!(!new_node.path_x.is_empty());
538 assert_eq!(new_node.path_x.len(), new_node.path_y.len());
539 assert_eq!(new_node.path_x.len(), new_node.path_yaw.len());
540 assert!(new_node.cost > 0.0);
541 assert!(new_node.parent.is_some());
542 }
543
544 #[test]
545 fn test_steer_endpoint_matches_dubins_goal() {
546 let obstacles = vec![];
547 let config = RRTDubinsConfig::default();
548 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
549 let mut planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
550 planner.start = RRTDubinsNode::new(0.0, 0.0, 0.0);
551 planner.goal = RRTDubinsNode::new(10.0, 10.0, 0.0);
552 planner.node_list = vec![planner.start.clone()];
553
554 let from = RRTDubinsNode::new(0.0, 0.0, 0.0);
555 let to = RRTDubinsNode::new(5.0, 0.0, 0.0);
556 let result = planner.steer(&from, &to, 0).unwrap();
557
558 assert!(approx_eq(result.x, 5.0, 0.3));
560 assert!(approx_eq(result.y, 0.0, 0.3));
561 }
562
563 #[test]
564 fn test_nearest_node_index() {
565 let obstacles = vec![];
566 let config = RRTDubinsConfig::default();
567 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
568 let mut planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
569 planner.node_list = vec![
570 RRTDubinsNode::new(0.0, 0.0, 0.0),
571 RRTDubinsNode::new(5.0, 5.0, 0.0),
572 RRTDubinsNode::new(10.0, 10.0, 0.0),
573 ];
574
575 let query = RRTDubinsNode::new(4.0, 4.0, 0.0);
576 assert_eq!(planner.get_nearest_node_index(&query), 1);
577
578 let query = RRTDubinsNode::new(9.0, 9.0, 0.0);
579 assert_eq!(planner.get_nearest_node_index(&query), 2);
580 }
581
582 #[test]
583 fn test_search_best_goal_node() {
584 let obstacles = vec![];
585 let config = RRTDubinsConfig {
586 goal_xy_threshold: 1.0,
587 goal_yaw_threshold: 0.5,
588 ..Default::default()
589 };
590 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
591 let mut planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
592 planner.goal = RRTDubinsNode::new(10.0, 10.0, 0.0);
593
594 planner.node_list = vec![RRTDubinsNode::new(0.0, 0.0, 0.0)];
596 assert!(planner.search_best_goal_node().is_none());
597
598 let mut n = RRTDubinsNode::new(10.0, 10.0, PI);
600 n.cost = 5.0;
601 planner.node_list.push(n);
602 assert!(planner.search_best_goal_node().is_none());
603
604 let mut n = RRTDubinsNode::new(10.2, 10.2, 0.1);
606 n.cost = 8.0;
607 planner.node_list.push(n);
608 assert_eq!(planner.search_best_goal_node(), Some(2));
609 }
610
611 #[test]
612 fn test_deterministic_planning_no_obstacles() {
613 let obstacles = vec![];
614 let config = RRTDubinsConfig {
615 curvature: 1.0,
616 goal_sample_rate: 10,
617 max_iter: 500,
618 goal_xy_threshold: 1.5,
619 goal_yaw_threshold: 0.5,
620 robot_radius: 0.0,
621 path_resolution: 0.3,
622 };
623 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
624 let mut planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
625
626 let start = Pose2D::new(0.0, 0.0, 0.0);
627 let goal = Pose2D::new(5.0, 5.0, 0.0);
628
629 let result = planner.plan_with_sampler(start, goal, |p| {
631 RRTDubinsNode::new(p.goal.x, p.goal.y, p.goal.yaw)
632 });
633
634 assert!(result.is_some(), "Should find a path with no obstacles");
635 let path = result.unwrap();
636 assert!(path.poses.len() >= 2);
637
638 let first = &path.poses[0];
640 let last = &path.poses[path.poses.len() - 1];
641 assert!(approx_eq(first.x, 0.0, 0.5));
642 assert!(approx_eq(first.y, 0.0, 0.5));
643 assert!(approx_eq(last.x, 5.0, 1.5));
644 assert!(approx_eq(last.y, 5.0, 1.5));
645 }
646
647 #[test]
648 fn test_planning_with_obstacles() {
649 let obstacles = create_obstacle_list();
650 let config = RRTDubinsConfig {
651 curvature: 1.0,
652 goal_sample_rate: 10,
653 max_iter: 1000,
654 goal_xy_threshold: 1.5,
655 goal_yaw_threshold: 1.0,
656 robot_radius: 0.0,
657 path_resolution: 0.3,
658 };
659 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
660 let mut planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
661
662 let start = Pose2D::new(0.0, 0.0, 0.0);
663 let goal = Pose2D::new(10.0, 10.0, 0.0);
664
665 let result = planner.planning(start, goal);
666 if let Some(path) = result {
670 assert!(path.poses.len() >= 2);
671 for pose in &path.poses {
673 assert!(pose.x.is_finite());
674 assert!(pose.y.is_finite());
675 assert!(pose.yaw.is_finite());
676 }
677 }
678 }
679
680 #[test]
681 fn test_sample_dubins_with_yaw_straight() {
682 let planner = DubinsPlanner::new(1.0);
683 let start = Pose2D::new(0.0, 0.0, 0.0);
684 let goal = Pose2D::new(5.0, 0.0, 0.0);
685 let path = planner.plan(start, goal).unwrap();
686
687 let (xs, ys, yaws) = sample_dubins_with_yaw(&path, 0.5);
688
689 assert!(xs.len() > 2);
690 assert_eq!(xs.len(), ys.len());
691 assert_eq!(xs.len(), yaws.len());
692
693 assert!(approx_eq(xs[0], 0.0, 1e-12));
695 assert!(approx_eq(ys[0], 0.0, 1e-12));
696
697 assert!(approx_eq(*xs.last().unwrap(), 5.0, 0.3));
699 assert!(approx_eq(*ys.last().unwrap(), 0.0, 0.3));
700
701 for &yaw in &yaws {
703 assert!(approx_eq(yaw, 0.0, 0.1));
704 }
705 }
706
707 #[test]
708 fn test_sample_dubins_with_yaw_turn() {
709 let planner = DubinsPlanner::new(1.0);
710 let start = Pose2D::new(0.0, 0.0, 0.0);
711 let goal = Pose2D::new(3.0, 3.0, PI / 2.0);
712 let path = planner.plan(start, goal).unwrap();
713
714 let (xs, _ys, yaws) = sample_dubins_with_yaw(&path, 0.1);
715
716 assert!(xs.len() > 2);
717 let end_yaw = *yaws.last().unwrap();
719 assert!(
720 approx_eq(angle_diff(end_yaw, PI / 2.0).abs(), 0.0, 0.3),
721 "end yaw {} should be near PI/2",
722 end_yaw
723 );
724 }
725
726 #[test]
727 fn test_generate_final_course() {
728 let obstacles = vec![];
729 let config = RRTDubinsConfig::default();
730 let rand_area = AreaBounds::new(-2.0, 15.0, -2.0, 15.0);
731 let mut planner = RRTDubinsPlanner::new(obstacles, rand_area, config);
732 planner.start = RRTDubinsNode::new(0.0, 0.0, 0.0);
733 planner.goal = RRTDubinsNode::new(10.0, 10.0, 0.0);
734
735 let root = RRTDubinsNode::new(0.0, 0.0, 0.0);
737 let mut child = RRTDubinsNode::new(5.0, 5.0, 0.5);
738 child.parent = Some(0);
739 child.path_x = vec![0.0, 2.5, 5.0];
740 child.path_y = vec![0.0, 2.5, 5.0];
741 child.path_yaw = vec![0.0, 0.25, 0.5];
742
743 planner.node_list = vec![root, child];
744
745 let course = planner.generate_final_course(1);
746 assert!(course.poses.len() >= 3);
748 assert!(approx_eq(course.poses[0].x, 0.0, 1e-12));
750 let last = course.poses.last().unwrap();
752 assert!(approx_eq(last.x, 10.0, 1e-12));
753 assert!(approx_eq(last.y, 10.0, 1e-12));
754 }
755}