diff --git a/.gitignore b/.gitignore
index c23fbf9..d3c0c0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,4 +33,8 @@
# Cmake Build Folders
build/
-test/build/
\ No newline at end of file
+test/build/
+
+# Demo generated files
+demo/*.csv
+demo/frames_flat/
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fae4257..686b5c9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -38,7 +38,7 @@ if(NOT Heuclid_FOUND)
include(FetchContent)
FetchContent_Declare(Heuclid
GIT_REPOSITORY "https://github.com/Mr-tooth/Heuclid.git"
- GIT_TAG "v2.1"
+ GIT_TAG "v2.2"
GIT_SHALLOW TRUE)
set(BUILD_TESTING_HEUCID OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(Heuclid)
@@ -137,17 +137,11 @@ if(MATPLOTLIB_CPP_AVAILABLE)
endif()
message(STATUS "matplotlib_cpp enabled — PlotCheck module included")
- # --- Demo GIF generator ---
- add_executable(demo_flat demo/demo_flat.cpp)
- target_link_libraries(demo_flat PRIVATE ${PROJECT_NAME})
- target_compile_definitions(demo_flat PRIVATE HAS_MATPLOTLIB)
- if(matplotlib_cpp_FOUND)
- target_include_directories(demo_flat PRIVATE ${matplotlib_cpp_INCLUDE_DIRS})
- target_link_libraries(demo_flat PRIVATE ${matplotlib_LIBS})
- else()
- target_include_directories(demo_flat PRIVATE ${MATPLOTLIB_CPP_INCLUDE})
- target_link_libraries(demo_flat PRIVATE Python3::Python Python3::NumPy)
- endif()
+ # --- Demo GIF generator (data export only, visualization in Python) ---
+ add_executable(demo_export demo/demo_export.cpp)
+ target_link_libraries(demo_export PRIVATE ${PROJECT_NAME})
+ add_executable(demo_obstacle demo/demo_obstacle.cpp)
+ target_link_libraries(demo_obstacle PRIVATE ${PROJECT_NAME})
else()
message(STATUS "matplotlib_cpp not found — PlotCheck module disabled")
endif()
diff --git a/README.md b/README.md
index 5a0e604..c53c72e 100644
--- a/README.md
+++ b/README.md
@@ -44,10 +44,18 @@ This project is a C++ reimplementation of the core algorithms from the [IHMC Foo
-> A* footstep planning from start (blue dot) to goal (red arrow). Red = left foot, orange = right foot. 19 discrete footsteps with body path (green dashed line).
+> A* footstep planning from start (blue dot) to goal (red diamond). Red = left foot, orange = right foot. 25 discrete footsteps evenly straddling the ellipsoid body path (blue line with direction arrows).
+
+### Obstacle Avoidance
+
+
+
+
+
+> Footsteps navigate around a gray obstacle block placed on the body path. The planner rejects footsteps intersecting the obstacle polygon, producing a natural detour (24 steps).
- ✅ Flat terrain: start → goal footstep sequence
-- ⏳ Obstacle avoidance: navigating around forbidden regions
+- ✅ Obstacle avoidance: navigating around forbidden regions
- ⏳ Stair climbing: constrained footstep planning on stairs
## Quick Start
diff --git a/README_CN.md b/README_CN.md
index c85be08..f092e92 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -44,10 +44,18 @@
-> A* 落脚点规划:从起点(蓝点)到目标点(红箭头)。红色=左脚,橙色=右脚。19 个离散落脚点 + 身体路径(绿色虚线)。
+> A* 落脚点规划:从起点(蓝色方块)到目标点(红色菱形)。红色=左脚,橙色=右脚。25 个离散落脚点均匀分布在椭圆身体路径(蓝色曲线,带方向箭头)两侧。
+
+### 障碍物避障
+
+
+
+
+
+> 落脚点绕过放置在身体路径上的灰色障碍物块。规划器会拒绝与障碍物多边形相交的落脚点,产生自然的绕行路径(24 步)。
- ✅ 平地:起点 → 终点落脚点序列
-- ⏳ 障碍物回避:绕开禁止区域
+- ✅ 障碍物回避:绕开禁止区域
- ⏳ 楼梯攀爬:楼梯约束下的落脚点规划
> - 障碍物避让:绕过禁区的路径规划
> - 楼梯场景:约束条件下的楼梯落脚点规划
diff --git a/assets/flat_terrain.gif b/assets/flat_terrain.gif
index c79a052..431a5d6 100644
Binary files a/assets/flat_terrain.gif and b/assets/flat_terrain.gif differ
diff --git a/assets/obstacle_avoidance.gif b/assets/obstacle_avoidance.gif
new file mode 100644
index 0000000..7bda024
Binary files /dev/null and b/assets/obstacle_avoidance.gif differ
diff --git a/demo/demo_export.cpp b/demo/demo_export.cpp
new file mode 100644
index 0000000..072a5af
--- /dev/null
+++ b/demo/demo_export.cpp
@@ -0,0 +1,135 @@
+/**
+ * Demo: Flat terrain footstep planning with body path.
+ * Uses test7 tuned parameters for optimal body path following.
+ * Exports real planner data to CSV for Python visualization.
+ */
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+using namespace ljh::heuclid;
+using namespace ljh::path::footstep_planner;
+
+int main()
+{
+ // === Flat terrain scenario (test7) ===
+ double startX = 0.015, startY = 0.0, startZ = 0.0, startYaw = 0.0;
+ double goalX = 0.663, goalY = -0.962, goalZ = 0.0, goalYaw = -1.554;
+
+ Pose2D goalPose2D(goalX, goalY, goalYaw);
+ Pose3D goalPose(goalX, goalY, goalZ, goalYaw, 0.0, 0.0);
+ Pose3D startPose(startX, startY, startZ, startYaw, 0.0, 0.0);
+
+ // === Body path (ellipsoid) ===
+ Simple2DBodyPathHolder pathHolder;
+ pathHolder.initialize({startX, startY, startYaw}, goalPose2D);
+ auto waypoints = pathHolder.getWayPointPath();
+
+ {
+ std::ofstream fout("demo/body_path.csv");
+ fout << "idx,x,y,yaw" << std::endl;
+ for (size_t i = 0; i < waypoints.size(); i++)
+ {
+ fout << i << ","
+ << waypoints[i].getPosition().getX() << ","
+ << waypoints[i].getPosition().getY() << ","
+ << waypoints[i].getOrientation().getYaw() << std::endl;
+ }
+ fout.close();
+ std::cout << "Wrote " << waypoints.size() << " body path waypoints" << std::endl;
+ }
+
+ AStarFootstepPlanner planner;
+
+ // Apply test7 tuned parameters
+ parameters param;
+ param.SetEdgeCostDistance(param, 4.0);
+ param.SetEdgeCostYaw(param, 4.0);
+ param.SetEdgeCostStaticPerStep(param, 1.4);
+ param.SetMaxStepYaw(param, pi / 12.0);
+ param.SetMinStepYaw(param, -pi / 12.0);
+ param.SetFinalTurnProximity(param, 0.3);
+ param.SetGoalDistanceProximity(param, 0.04);
+ param.SetGoalYawProximity(param, 4.0 / 180.0 * pi);
+ param.SetFootPolygonExtendedLength(param, 0.025);
+
+ // HWP weights — test7 tuned values
+ param.SetHWPOfWalkDistacne(param, 1.30);
+ // Enable body path following via heuristic
+ param.SetFollowBodyPath(param, true);
+ param.SetHWPOfPathDistance(param, 1.0); // Body path heuristic weight
+ param.SetEdgeCostPathDev(param, 0.0); // No edge penalty needed — heuristic handles it
+ param.SetHWPOfInitialTurnDistacne(param, 1.0);
+ param.SetHWPOfFinalTurnDistacne(param, 1.30);
+ param.SetHWPOfFinalWalkDistacne(param, 1.30);
+
+ // Step size constraints
+ param.SetMaxStepLength(param, 0.08);
+ param.SetMinStepLength(param, -0.08);
+ param.SetMaxStepWidth(param, 0.22);
+ param.SetMinStepWidth(param, 0.16);
+ param.SetMaxStepReach(param, sqrt(pow(0.22 - 0.16, 2) + 0.08 * 0.08));
+
+ // Params are static members — set once, used everywhere
+
+ planner.initialize(goalPose2D, goalPose, startPose);
+ planner.doAStarSearch();
+ planner.calFootstepSeries();
+ auto accurateSteps = planner.getOrCalAccurateFootstepSeries();
+
+ std::cout << "Accurate footsteps: " << accurateSteps.size() << std::endl;
+
+ // === Export footstep center positions ===
+ {
+ std::ofstream fout("demo/footsteps.csv");
+ fout << "step,x,y,yaw,side" << std::endl;
+ for (size_t i = 0; i < accurateSteps.size(); i++)
+ {
+ auto& s = accurateSteps[i];
+ std::string side = (s.getStepFlag() == stepL) ? "L" : "R";
+ fout << i << "," << s.getX() << "," << s.getY() << ","
+ << s.getYaw() << "," << side << std::endl;
+ }
+ fout.close();
+ std::cout << "Wrote " << accurateSteps.size() << " footsteps" << std::endl;
+ }
+
+ // === Export foot polygon vertices ===
+ {
+ std::ofstream fout("demo/foot_polygons.csv");
+ fout << "step,vertex,x,y" << std::endl;
+ for (size_t i = 0; i < accurateSteps.size(); i++)
+ {
+ auto& s = accurateSteps[i];
+ Pose2D pose;
+ pose.setPosition(s.getX(), s.getY());
+ pose.setOrientation(s.getYaw());
+
+ std::vector vx, vy;
+ getFootVertex2D(pose, s.getStepFlag(), vx, vy);
+
+ for (size_t j = 0; j < vx.size(); j++)
+ fout << i << "," << j << "," << vx[j] << "," << vy[j] << std::endl;
+ }
+ fout.close();
+ std::cout << "Wrote foot polygons" << std::endl;
+ }
+
+ // === Export start/goal ===
+ {
+ std::ofstream fout("demo/start_goal.csv");
+ fout << "pose,x,y,yaw" << std::endl;
+ fout << "start," << startX << "," << startY << "," << startYaw << std::endl;
+ fout << "goal," << goalX << "," << goalY << "," << goalYaw << std::endl;
+ fout.close();
+ }
+
+ std::cout << "All data exported." << std::endl;
+ return 0;
+}
diff --git a/demo/demo_flat.cpp b/demo/demo_flat.cpp
deleted file mode 100644
index 2f0fdca..0000000
--- a/demo/demo_flat.cpp
+++ /dev/null
@@ -1,163 +0,0 @@
-// Copyright 2026 Junhang Li
-// SPDX-License-Identifier: Apache-2.0
-
-// Demo: Flat terrain footstep planning with ellipsoid body path
-// Agg-compatible: uses only plt::plot + plt::annotate (no arrow, no scatter, no set_aspect_equal)
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace plt = matplotlibcpp;
-namespace fs = std::filesystem;
-
-void drawEllipsoidPath(ljh::path::footstep_planner::Simple2DBodyPathHolder& pathHolder)
-{
- auto waypoints = pathHolder.getWayPointPath();
- if (waypoints.empty()) return;
-
- std::vector x, y;
- for (size_t i = 0; i < waypoints.size(); i++)
- {
- x.push_back(waypoints[i].getPosition().getX());
- y.push_back(waypoints[i].getPosition().getY());
- }
-
- plt::plot(x, y, {{"color", "3498db"}, {"linewidth", "1.2"}});
-}
-
-void drawFootsteps(const std::vector& steps, int count)
-{
- std::vector vx, vy;
- for (int i = 0; i < count && i < (int)steps.size(); i++)
- {
- vx.clear();
- vy.clear();
- getFootVertex2D(steps.at(i), vx, vy);
- if (!vx.empty()) { vx.push_back(vx.front()); vy.push_back(vy.front()); }
-
- std::string color = (steps.at(i).getSecondStepSide().getStepFlag() == stepL) ? "e74c3c" : "f39c12";
- plt::plot(vx, vy, {{"color", color}, {"linewidth", "1.5"}});
-
- // Step center dot (instead of scatter)
- double cx = steps.at(i).getSecondStep().getX();
- double cy = steps.at(i).getSecondStep().getY();
- plt::plot(std::vector{cx}, std::vector{cy},
- {{"color", "3498db"}, {"marker", "."}, {"linestyle", "none"}, {"markersize", "4"}});
- }
-}
-
-void drawMarkers(double sx, double sy, double syaw,
- double gx, double gy, double gyaw)
-{
- const double arrowLen = 0.03;
-
- // Start: green dot + direction line
- plt::plot(std::vector{sx}, std::vector{sy},
- {{"color", "2ecc71"}, {"marker", "s"}, {"linestyle", "none"}, {"markersize", "10"}});
- plt::plot(std::vector{sx, sx + cos(syaw) * arrowLen},
- std::vector{sy, sy + sin(syaw) * arrowLen},
- {{"color", "2ecc71"}, {"linewidth", "3.0"}});
-
- // Goal: red dot + direction line
- plt::plot(std::vector{gx}, std::vector{gy},
- {{"color", "e74c3c"}, {"marker", "D"}, {"linestyle", "none"}, {"markersize", "10"}});
- plt::plot(std::vector{gx, gx + cos(gyaw) * arrowLen},
- std::vector{gy, gy + sin(gyaw) * arrowLen},
- {{"color", "e74c3c"}, {"linewidth", "3.0"}});
-}
-
-int main()
-{
- std::cout << "=== AStar Footstep Planner - Flat Terrain Demo ===" << std::endl;
-
- std::string outDir = "demo/frames_flat";
- fs::create_directories(outDir);
-
- ljh::path::footstep_planner::LatticePoint latticepoint;
- ljh::path::footstep_planner::parameters param;
- latticepoint.setGridSizeXY(latticepoint, 0.01);
- latticepoint.setYawDivision(latticepoint, 72);
- param.SetEdgeCostDistance(param, 4.0);
- param.SetEdgeCostYaw(param, 4.0);
- param.SetEdgeCostStaticPerStep(param, 1.4);
- param.SetDebugFlag(param, false);
- param.SetMaxStepYaw(param, pi / 12);
- param.SetMinStepYaw(param, -pi / 12);
- param.SetFinalTurnProximity(param, 0.3);
- param.SetGoalDistanceProximity(param, 0.04);
- param.SetGoalYawProximity(param, 4.0 / 180.0 * pi);
- param.SetFootPolygonExtendedLength(param, 0.025);
- param.SetHWPOfWalkDistacne(param, 1.30);
- param.SetHWPOfPathDistance(param, 2.50);
- param.SetHWPOfFinalTurnDistacne(param, 1.30);
- param.SetHWPOfFinalWalkDistacne(param, 1.30);
- param.SetMaxStepLength(param, 0.08);
- param.SetMinStepLength(param, -0.08);
- param.SetMaxStepWidth(param, 0.22);
- param.SetMinStepWidth(param, 0.16);
- param.SetMaxStepReach(param, sqrt(
- (param.MaxStepWidth - param.MinStepWidth) * (param.MaxStepWidth - param.MinStepWidth)
- + param.MaxStepLength * param.MaxStepLength));
-
- double startX = 0.015, startY = 0.0, startZ = 0.0, startYaw = 0.0;
- double goalX = 0.815, goalY = -0.8, goalZ = 0.0, goalYaw = -90.0 / 180.0 * pi;
-
- ljh::heuclid::Pose2D goalPose2D(goalX, goalY, goalYaw);
- ljh::heuclid::Pose3D goalPose(goalX, goalY, goalZ, goalYaw, 0.0, 0.0);
- ljh::heuclid::Pose3D startPose(startX, startY, startZ, startYaw, 0.0, 0.0);
-
- ljh::path::footstep_planner::Simple2DBodyPathHolder pathHolder;
- pathHolder.initialize({startX, startY, startYaw}, goalPose2D);
-
- std::cout << "Running A* search..." << std::endl;
- ljh::path::footstep_planner::AStarFootstepPlanner planner;
- planner.initialize(goalPose2D, goalPose, startPose);
- planner.doAStarSearch();
- planner.calFootstepSeries();
- auto outcome = planner.getFootstepSeries();
-
- std::cout << "Footsteps: " << outcome.size() << std::endl;
- if (outcome.empty()) { return 1; }
-
- int totalSteps = (int)outcome.size();
- int totalFrames = totalSteps + 1;
- std::cout << "Generating " << totalFrames << " frames..." << std::endl;
-
- for (int n = 0; n < totalFrames; n++)
- {
- int stepsToShow = n;
-
- plt::figure_size(900, 700);
-
- // Ellipsoid body path (always visible)
- drawEllipsoidPath(pathHolder);
-
- // Footsteps up to current step
- if (stepsToShow > 0)
- {
- drawFootsteps(outcome, stepsToShow);
- }
-
- // Start/goal markers ON TOP
- drawMarkers(startX, startY, startYaw, goalX, goalY, goalYaw);
-
- std::ostringstream fname;
- fname << outDir << "/frame_" << std::setw(3) << std::setfill('0') << n << ".png";
- plt::save(fname.str());
- plt::close();
-
- if (n % 5 == 0 || n == totalFrames - 1)
- std::cout << " Frame " << n << "/" << totalFrames - 1 << std::endl;
- }
-
- std::cout << "Done!" << std::endl;
- return 0;
-}
diff --git a/demo/demo_flat.py b/demo/demo_flat.py
deleted file mode 100644
index a8560a5..0000000
--- a/demo/demo_flat.py
+++ /dev/null
@@ -1,199 +0,0 @@
-#!/usr/bin/env python3
-"""
-Flat terrain demo: generates progressive footstep planning GIF.
-Uses planner binary output + Python matplotlib for visualization.
-"""
-
-import subprocess
-import re
-import os
-import sys
-
-def run_planner():
- """Run the compiled planner binary and parse footstep output."""
- build_dir = "build"
- binary = os.path.join(build_dir, "footstep_test")
-
- result = subprocess.run([binary], capture_output=True, text=True)
- output = result.stdout + result.stderr
-
- # Parse "First x y yaw" line from output
- # Output contains footstep series after A* search
- print("Planner output:")
- for line in output.split('\n'):
- if line.strip():
- print(f" {line}")
-
- return output
-
-def generate_frames_and_gif():
- """Generate frames using Python matplotlib."""
- try:
- import matplotlib
- matplotlib.use('Agg')
- import matplotlib.pyplot as plt
- import matplotlib.patches as patches
- from matplotlib.patches import Polygon as MplPolygon
- import numpy as np
- except ImportError:
- os.system(f"{sys.executable} -m pip install --break-system-packages matplotlib")
- import matplotlib
- matplotlib.use('Agg')
- import matplotlib.pyplot as plt
- import matplotlib.patches as patches
- from matplotlib.patches import Polygon as MplPolygon
- import numpy as np
-
- # Flat terrain scenario parameters (same as test.cpp)
- startX, startY, startYaw = 0.015, 0.0, 0.0
- goalX, goalY, goalYaw = 0.815, -0.8, -90.0 * np.pi / 180.0
-
- # Run planner and extract footsteps
- # For now, use known test data from the planner
- # Test 7 output: 19 footsteps from (0.015, 0) to (0.815, -0.8)
- # We'll use the actual planner binary output
-
- # Step dimensions (from parameters)
- max_step_length = 0.08
- max_step_width = 0.22
- min_step_width = 0.16
-
- # Generate ellipsoid body path
- a = goalX - startX # x extent
- b = goalY - startY # y extent
- xc = startX # center of ellipsoid
- yc = b
-
- theta = np.linspace(0, np.pi, 100)
- ell_x = xc + a * np.sqrt(np.maximum(0, 1 - (theta - np.pi/2)**2 / (np.pi/2)**2))
- # Actually, use the proper ellipsoid equation
- ell_x = []
- ell_y = []
- for i in range(100):
- t = i / 99.0
- y_val = startY + t * (goalY - startY)
- if abs(b) > 1e-6:
- ratio = (y_val - yc) / b
- if abs(ratio) <= 1.0:
- x_val = xc + a * np.sqrt(max(0, 1 - ratio**2))
- ell_x.append(x_val)
- ell_y.append(y_val)
-
- # Approximate footstep positions (19 steps from planner)
- # Using the test.cpp scenario output pattern
- footsteps = []
- n_steps = 19
- for i in range(n_steps):
- t = (i + 1) / (n_steps + 1)
- # Interpolate along the ellipsoid path
- idx = int(t * (len(ell_x) - 1)) if ell_x else 0
- if idx < len(ell_x):
- fx = ell_x[idx]
- fy = ell_y[idx]
- else:
- fx = startX + t * (goalX - startX)
- fy = startY + t * (goalY - startY)
- # Alternating left/right, offset by step width
- side = 'L' if i % 2 == 0 else 'R'
- offset = 0.04 if side == 'L' else -0.04
- fyaw = np.arctan2(goalY - startY, goalX - startX) * (1 - t) + goalYaw * t
- footsteps.append((fx, fy, fyaw, side))
-
- outDir = "demo/frames_flat"
- os.makedirs(outDir, exist_ok=True)
-
- foot_w = 0.02 # half-width of foot polygon
- foot_h = 0.04 # half-height of foot polygon
-
- totalFrames = len(footsteps) + 1
-
- for n in range(totalFrames):
- fig, ax = plt.subplots(1, 1, figsize=(9, 7))
- ax.set_aspect('equal')
- ax.set_xlim(-0.15, 1.05)
- ax.set_ylim(-1.25, 0.25)
- ax.set_axis_off()
-
- # Draw ellipsoid body path
- if ell_x and ell_y:
- ax.plot(ell_x, ell_y, color='#3498db', linewidth=1.2, alpha=0.7,
- label='Body path (ellipsoid)')
-
- # Draw footsteps up to current frame
- for i in range(n):
- fx, fy, fyaw, side = footsteps[i]
- # Foot polygon (rectangle rotated by fyaw)
- cos_a, sin_a = np.cos(fyaw), np.sin(fyaw)
- corners = [(-foot_w, -foot_h), (foot_w, -foot_h),
- (foot_w, foot_h), (-foot_w, foot_h)]
- rot_corners = []
- for cx, cy in corners:
- rx = fx + cx * cos_a - cy * sin_a
- ry = fy + cx * sin_a + cy * cos_a
- rot_corners.append((rx, ry))
-
- color = '#e74c3c' if side == 'L' else '#f39c12'
- poly = MplPolygon(rot_corners, closed=True,
- fill=False, edgecolor=color, linewidth=1.5)
- ax.add_patch(poly)
-
- # Step number
- ax.annotate(str(i), (fx + 0.02, fy + 0.02), fontsize=7, color='#2c3e50')
-
- # Draw start/goal markers
- ax.plot(startX, startY, 's', color='#2ecc71', markersize=10, zorder=5)
- ax.plot([startX, startX + np.cos(startYaw) * 0.035],
- [startY, startY + np.sin(startYaw) * 0.035],
- color='#2ecc71', linewidth=3)
- ax.text(startX - 0.05, startY + 0.03, 'Start', fontsize=9, color='#2ecc71',
- fontweight='bold')
-
- ax.plot(goalX, goalY, 'D', color='#e74c3c', markersize=10, zorder=5)
- ax.plot([goalX, goalX + np.cos(goalYaw) * 0.035],
- [goalY, goalY + np.sin(goalYaw) * 0.035],
- color='#e74c3c', linewidth=3)
- ax.text(goalX - 0.03, goalY + 0.03, 'Goal', fontsize=9, color='#e74c3c',
- fontweight='bold')
-
- # Title
- ax.set_title('A* Footstep Planning — Flat Terrain', fontsize=13, fontweight='bold')
-
- # Legend (only on last frame)
- if n == totalFrames - 1:
- from matplotlib.lines import Line2D
- legend_elements = [
- Line2D([0], [0], color='#3498db', linewidth=1.2, label='Body path'),
- Line2D([0], [0], color='#e74c3c', linewidth=1.5, label='Left foot'),
- Line2D([0], [0], color='#f39c12', linewidth=1.5, label='Right foot'),
- ]
- ax.legend(handles=legend_elements, loc='upper right', fontsize=8)
-
- # Step counter
- step_label = f"{n} / {len(footsteps)}" if n > 0 else f"0 / {len(footsteps)}"
- ax.text(0.0, -1.18, f"Step {step_label}", fontsize=10, color='#7f8c8d',
- fontfamily='monospace')
-
- fname = f"{outDir}/frame_{n:03d}.png"
- plt.savefig(fname, dpi=100, bbox_inches='tight', facecolor='white')
- plt.close()
-
- if n % 5 == 0 or n == totalFrames - 1:
- print(f" Frame {n}/{totalFrames - 1}")
-
- # Generate GIF
- from PIL import Image
- frames = sorted([f for f in os.listdir(outDir) if f.endswith('.png')])
- images = [Image.open(os.path.join(outDir, f)) for f in frames]
-
- durations = [300] * (len(images) - 1) + [2000] # Hold last frame longer
- gif_path = "demo/flat_terrain.gif"
- images[0].save(gif_path, save_all=True, append_images=images[1:],
- duration=durations, loop=0, optimize=True)
-
- size_kb = os.path.getsize(gif_path) / 1024
- print(f"\nGIF saved: {gif_path} ({size_kb:.0f} KB)")
-
-
-if __name__ == "__main__":
- print("=== AStar Footstep Planner - Flat Terrain Demo ===")
- generate_frames_and_gif()
diff --git a/demo/demo_obstacle.cpp b/demo/demo_obstacle.cpp
new file mode 100644
index 0000000..84f9e11
--- /dev/null
+++ b/demo/demo_obstacle.cpp
@@ -0,0 +1,156 @@
+/**
+ * Demo: Obstacle avoidance footstep planning.
+ * Uses the stair polygon mechanism as an obstacle region placed
+ * between start and goal, forcing the planner to route around it.
+ */
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+using namespace ljh::heuclid;
+using namespace ljh::path::footstep_planner;
+
+int main()
+{
+ // === Flat terrain with obstacle ===
+ // Obstacle placed at center of path to force detour
+ double startX = 0.015, startY = 0.0, startZ = 0.0, startYaw = 0.0;
+ double goalX = 0.815, goalY = -0.8, goalZ = 0.0, goalYaw = -M_PI / 2.0;
+
+ Pose2D goalPose2D(goalX, goalY, goalYaw);
+ Pose3D goalPose(goalX, goalY, goalZ, goalYaw, 0.0, 0.0);
+ Pose3D startPose(startX, startY, startZ, startYaw, 0.0, 0.0);
+
+ // === Body path (ellipsoid) ===
+ Simple2DBodyPathHolder pathHolder;
+ pathHolder.initialize({startX, startY, startYaw}, goalPose2D);
+ auto waypoints = pathHolder.getWayPointPath();
+
+ {
+ std::ofstream fout("demo/body_path.csv");
+ fout << "idx,x,y,yaw" << std::endl;
+ for (size_t i = 0; i < waypoints.size(); i++)
+ {
+ fout << i << ","
+ << waypoints[i].getPosition().getX() << ","
+ << waypoints[i].getPosition().getY() << ","
+ << waypoints[i].getOrientation().getYaw() << std::endl;
+ }
+ fout.close();
+ }
+
+ // === Obstacle polygon (stair polygon used as obstacle) ===
+ // A rectangle blocking the body path in the middle area
+ // The obstacle is placed at the body path center to force detour
+ double obsX = 0.35, obsY = -0.38;
+ double obsW = 0.12, obsH = 0.12; // 12cm x 12cm obstacle
+ std::vector> obstacle({
+ {obsX - obsW/2, obsY - obsH/2},
+ {obsX + obsW/2, obsY - obsH/2},
+ {obsX + obsW/2, obsY + obsH/2},
+ {obsX - obsW/2, obsY + obsH/2}
+ });
+
+ {
+ std::ofstream fout("demo/obstacle.csv");
+ fout << "x,y" << std::endl;
+ for (auto& p : obstacle)
+ fout << p.getX() << "," << p.getY() << std::endl;
+ // close polygon
+ fout << obstacle[0].getX() << "," << obstacle[0].getY() << std::endl;
+ fout.close();
+ }
+
+ // === Run planner ===
+ parameters param;
+ param.SetEdgeCostDistance(param, 4.0);
+ param.SetEdgeCostYaw(param, 4.0);
+ param.SetEdgeCostStaticPerStep(param, 1.4);
+ param.SetMaxStepYaw(param, pi / 12.0);
+ param.SetMinStepYaw(param, -pi / 12.0);
+ param.SetFinalTurnProximity(param, 0.3);
+ param.SetGoalDistanceProximity(param, 0.04);
+ param.SetGoalYawProximity(param, 4.0 / 180.0 * pi);
+ param.SetFootPolygonExtendedLength(param, 0.025);
+
+ // HWP weights — body path following
+ param.SetHWPOfWalkDistacne(param, 1.30);
+ param.SetHWPOfPathDistance(param, 1.0);
+ param.SetHWPOfInitialTurnDistacne(param, 1.0);
+ param.SetHWPOfFinalTurnDistacne(param, 1.30);
+ param.SetHWPOfFinalWalkDistacne(param, 1.30);
+
+ // Step size constraints
+ param.SetMaxStepLength(param, 0.08);
+ param.SetMinStepLength(param, -0.08);
+ param.SetMaxStepWidth(param, 0.22);
+ param.SetMinStepWidth(param, 0.16);
+ param.SetMaxStepReach(param, sqrt(pow(0.22 - 0.16, 2) + 0.08 * 0.08));
+
+ // Enable body path following
+ param.SetFollowBodyPath(param, true);
+
+ // Enable stair align mode to activate obstacle (stair polygon) blocking
+ param.SetStairAlignMode(param, true);
+ param.SetStairPolygon(param, obstacle, 4, 0);
+
+ AStarFootstepPlanner planner;
+ planner.initialize(goalPose2D, goalPose, startPose);
+ planner.doAStarSearch();
+ planner.calFootstepSeries();
+ auto accurateSteps = planner.getOrCalAccurateFootstepSeries();
+
+ std::cout << "Accurate footsteps: " << accurateSteps.size() << std::endl;
+
+ // === Export footsteps ===
+ {
+ std::ofstream fout("demo/footsteps.csv");
+ fout << "step,x,y,yaw,side" << std::endl;
+ for (size_t i = 0; i < accurateSteps.size(); i++)
+ {
+ auto& s = accurateSteps[i];
+ std::string side = (s.getStepFlag() == stepL) ? "L" : "R";
+ fout << i << "," << s.getX() << "," << s.getY() << ","
+ << s.getYaw() << "," << side << std::endl;
+ }
+ fout.close();
+ }
+
+ // === Export foot polygons ===
+ {
+ std::ofstream fout("demo/foot_polygons.csv");
+ fout << "step,vertex,x,y" << std::endl;
+ for (size_t i = 0; i < accurateSteps.size(); i++)
+ {
+ auto& s = accurateSteps[i];
+ Pose2D pose;
+ pose.setPosition(s.getX(), s.getY());
+ pose.setOrientation(s.getYaw());
+
+ std::vector vx, vy;
+ getFootVertex2D(pose, s.getStepFlag(), vx, vy);
+
+ for (size_t j = 0; j < vx.size(); j++)
+ fout << i << "," << j << "," << vx[j] << "," << vy[j] << std::endl;
+ }
+ fout.close();
+ }
+
+ // === Export start/goal ===
+ {
+ std::ofstream fout("demo/start_goal.csv");
+ fout << "pose,x,y,yaw" << std::endl;
+ fout << "start," << startX << "," << startY << "," << startYaw << std::endl;
+ fout << "goal," << goalX << "," << goalY << "," << goalYaw << std::endl;
+ fout.close();
+ }
+
+ std::cout << "All data exported." << std::endl;
+ return 0;
+}
diff --git a/demo/demo_visualize.py b/demo/demo_visualize.py
new file mode 100644
index 0000000..08a938d
--- /dev/null
+++ b/demo/demo_visualize.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+"""
+Flat terrain demo: generates progressive footstep planning GIF.
+Uses real planner data exported by demo_export.cpp.
+"""
+import os
+import sys
+import csv
+import numpy as np
+
+try:
+ import matplotlib
+ matplotlib.use('Agg')
+ import matplotlib.pyplot as plt
+ from matplotlib.patches import Polygon as MplPolygon
+ from matplotlib.lines import Line2D
+except ImportError:
+ os.system(f"{sys.executable} -m pip install --break-system-packages matplotlib")
+ import matplotlib
+ matplotlib.use('Agg')
+ import matplotlib.pyplot as plt
+ from matplotlib.patches import Polygon as MplPolygon
+ from matplotlib.lines import Line2D
+
+
+def load_csv(path):
+ with open(path) as f:
+ return list(csv.DictReader(f))
+
+
+def main():
+ # Load planner data
+ footsteps_raw = load_csv("demo/footsteps.csv")
+ body_path_raw = load_csv("demo/body_path.csv")
+ foot_poly_raw = load_csv("demo/foot_polygons.csv")
+ sg_raw = load_csv("demo/start_goal.csv")
+
+ # Parse footsteps
+ footsteps = []
+ for row in footsteps_raw:
+ footsteps.append({
+ 'x': float(row['x']),
+ 'y': float(row['y']),
+ 'yaw': float(row['yaw']),
+ 'side': row['side'],
+ 'step': int(row['step']),
+ })
+ n_steps = len(footsteps)
+ print(f"Loaded {n_steps} footsteps")
+
+ # Parse body path
+ body_x = [float(r['x']) for r in body_path_raw]
+ body_y = [float(r['y']) for r in body_path_raw]
+
+ # Parse foot polygons (group by step)
+ foot_poly = {}
+ for row in foot_poly_raw:
+ sid = int(row['step'])
+ if sid not in foot_poly:
+ foot_poly[sid] = {'x': [], 'y': []}
+ foot_poly[sid]['x'].append(float(row['x']))
+ foot_poly[sid]['y'].append(float(row['y']))
+
+ # Load obstacle polygon (optional)
+ obstacle_x, obstacle_y = [], []
+ if os.path.exists("demo/obstacle.csv"):
+ with open("demo/obstacle.csv") as f:
+ for row in csv.DictReader(f):
+ obstacle_x.append(float(row["x"]))
+ obstacle_y.append(float(row["y"]))
+ print(f"Loaded obstacle polygon ({len(obstacle_x)} vertices)")
+
+ # Parse start/goal
+ start_x, start_y, start_yaw = 0, 0, 0
+ goal_x, goal_y, goal_yaw = 0, 0, 0
+ for row in sg_raw:
+ if row['pose'] == 'start':
+ start_x, start_y, start_yaw = float(row['x']), float(row['y']), float(row['yaw'])
+ else:
+ goal_x, goal_y, goal_yaw = float(row['x']), float(row['y']), float(row['yaw'])
+
+ # Colors
+ COLOR_L = '#e74c3c' # red
+ COLOR_R = '#f39c12' # orange/amber
+ COLOR_BODY = '#3498db'
+ COLOR_START = '#2ecc71'
+ COLOR_GOAL = '#e74c3c'
+
+ outDir = "demo/frames_flat"
+ os.makedirs(outDir, exist_ok=True)
+
+ # Determine axis limits from data
+ all_x = body_x + [s['x'] for s in footsteps] + [start_x, goal_x]
+ all_y = body_y + [s['y'] for s in footsteps] + [start_y, goal_y]
+ margin = 0.15
+ x_min, x_max = min(all_x) - margin, max(all_x) + margin
+ y_min, y_max = min(all_y) - margin, max(all_y) + margin
+
+ total_frames = n_steps + 1 # frame 0 = no footsteps, then add one per frame
+
+ for frame in range(total_frames):
+ fig, ax = plt.subplots(1, 1, figsize=(10, 8))
+ ax.set_aspect('equal')
+ ax.set_xlim(x_min, x_max)
+ ax.set_ylim(y_min, y_max)
+ ax.set_axis_off()
+
+ # Draw obstacle polygon
+ if obstacle_x and obstacle_y:
+ obs_patch = MplPolygon(list(zip(obstacle_x, obstacle_y)),
+ closed=True, facecolor="#2c3e50", edgecolor="#c0392b",
+ alpha=0.4, linewidth=2, zorder=2.5, label="Obstacle")
+ ax.add_patch(obs_patch)
+ obs_cx, obs_cy = sum(obstacle_x)/len(obstacle_x), sum(obstacle_y)/len(obstacle_y)
+ ax.text(obs_cx, obs_cy, "✕", fontsize=14, color="white",
+ ha="center", va="center", fontweight="bold", zorder=3)
+
+ # Draw body path (ellipsoid)
+ ax.plot(body_x, body_y, color=COLOR_BODY, linewidth=1.5, alpha=0.6, linestyle='-',
+ label='Body path (ellipsoid)', zorder=1)
+
+ # Draw body path direction arrows
+ arrow_step = max(1, len(body_path_raw) // 8)
+ bw = 0.015
+ for i in range(0, len(body_path_raw), arrow_step):
+ bx = float(body_path_raw[i]['x'])
+ by = float(body_path_raw[i]['y'])
+ byaw = float(body_path_raw[i]['yaw'])
+ ax.plot([bx, bx + np.cos(byaw)*bw],
+ [by, by + np.sin(byaw)*bw],
+ color=COLOR_BODY, lw=1.8, alpha=0.7, zorder=2)
+ ax.plot(bx + np.cos(byaw)*bw, by + np.sin(byaw)*bw, '>',
+ color=COLOR_BODY, markersize=5, alpha=0.7, zorder=2)
+
+ # Draw footsteps up to current frame
+ for i in range(frame):
+ s = footsteps[i]
+ color = COLOR_L if s['side'] == 'L' else COLOR_R
+
+ # Draw foot polygon
+ if i in foot_poly:
+ poly = MplPolygon(list(zip(foot_poly[i]['x'], foot_poly[i]['y'])),
+ closed=True, facecolor=color, edgecolor='white',
+ alpha=0.65, linewidth=1.2, zorder=3)
+ ax.add_patch(poly)
+
+ # Step number (offset from foot center)
+ offset_y = 0.035 if s['side'] == 'L' else -0.035
+ ax.text(s['x'] + 0.025, s['y'] + offset_y, str(i), fontsize=8,
+ color='#2c3e50', fontweight='bold', zorder=4)
+
+ # Draw start/goal markers LAST (on top) to prevent jumping
+ ax.plot(start_x, start_y, 's', color=COLOR_START, markersize=12, zorder=6)
+ ax.annotate('', xy=(start_x + np.cos(start_yaw)*0.04, start_y + np.sin(start_yaw)*0.04),
+ xytext=(start_x, start_y),
+ arrowprops=dict(arrowstyle='->', color=COLOR_START, lw=3),
+ zorder=6)
+ ax.text(start_x, start_y + 0.06, 'Start', fontsize=10, color=COLOR_START,
+ fontweight='bold', ha='center', zorder=6,
+ bbox=dict(boxstyle='round,pad=0.15', facecolor='white', edgecolor='none', alpha=0.85))
+
+ ax.plot(goal_x, goal_y, 'D', color=COLOR_GOAL, markersize=12, zorder=6)
+ ax.annotate('', xy=(goal_x + np.cos(goal_yaw)*0.04, goal_y + np.sin(goal_yaw)*0.04),
+ xytext=(goal_x, goal_y),
+ arrowprops=dict(arrowstyle='->', color=COLOR_GOAL, lw=3),
+ zorder=6)
+ ax.text(goal_x, goal_y + 0.07, 'Goal', fontsize=10, color=COLOR_GOAL,
+ fontweight='bold', ha='center', zorder=6,
+ bbox=dict(boxstyle='round,pad=0.15', facecolor='white', edgecolor='none', alpha=0.85))
+
+ # Title
+ ax.set_title('A* Footstep Planning — Flat Terrain', fontsize=14, fontweight='bold', pad=10)
+
+ # Legend
+ legend_elements = [
+ Line2D([0], [0], color=COLOR_BODY, linewidth=1.5, label='Body path (ellipsoid)'),
+ Line2D([0], [0], color=COLOR_L, linewidth=2, label='Left foot'),
+ Line2D([0], [0], color=COLOR_R, linewidth=2, label='Right foot'),
+ Line2D([0], [0], color="#c0392b", linewidth=2, label="Obstacle", fill=True, alpha=0.4),
+ ]
+ ax.legend(handles=legend_elements, loc='lower left', fontsize=9, framealpha=0.9)
+
+ # Step counter
+ step_label = f"{frame} / {n_steps}" if frame > 0 else f"0 / {n_steps}"
+ ax.text(0.98, 0.02, f"Step {step_label}", transform=ax.transAxes,
+ fontsize=11, color='#7f8c8d', fontfamily='monospace',
+ ha='right', va='bottom')
+
+ fname = f"{outDir}/frame_{frame:03d}.png"
+ plt.savefig(fname, dpi=100, bbox_inches='tight', facecolor='white')
+ plt.close()
+
+ if frame % 3 == 0 or frame == total_frames - 1:
+ print(f" Frame {frame}/{total_frames - 1}")
+
+ # Generate GIF
+ from PIL import Image
+ frames = sorted([f for f in os.listdir(outDir) if f.endswith('.png')])
+ images = [Image.open(os.path.join(outDir, f)) for f in frames]
+
+ durations = [300] * (len(images) - 1) + [2000]
+ gif_path = "demo/flat_terrain.gif"
+ images[0].save(gif_path, save_all=True, append_images=images[1:],
+ duration=durations, loop=0, optimize=True)
+
+ size_kb = os.path.getsize(gif_path) / 1024
+ print(f"\nGIF saved: {gif_path} ({size_kb:.0f} KB)")
+
+
+if __name__ == "__main__":
+ print("=== A* Footstep Planner - Flat Terrain Demo (Real Planner Data) ===")
+ main()
diff --git a/demo/flat_terrain.gif b/demo/flat_terrain.gif
index c79a052..431a5d6 100644
Binary files a/demo/flat_terrain.gif and b/demo/flat_terrain.gif differ
diff --git a/demo/frames_flat/frame_000.png b/demo/frames_flat/frame_000.png
deleted file mode 100644
index 8250375..0000000
Binary files a/demo/frames_flat/frame_000.png and /dev/null differ
diff --git a/demo/frames_flat/frame_001.png b/demo/frames_flat/frame_001.png
deleted file mode 100644
index 9a30e24..0000000
Binary files a/demo/frames_flat/frame_001.png and /dev/null differ
diff --git a/demo/frames_flat/frame_002.png b/demo/frames_flat/frame_002.png
deleted file mode 100644
index a99b3b8..0000000
Binary files a/demo/frames_flat/frame_002.png and /dev/null differ
diff --git a/demo/frames_flat/frame_003.png b/demo/frames_flat/frame_003.png
deleted file mode 100644
index 3e74d83..0000000
Binary files a/demo/frames_flat/frame_003.png and /dev/null differ
diff --git a/demo/frames_flat/frame_004.png b/demo/frames_flat/frame_004.png
deleted file mode 100644
index 443c1fe..0000000
Binary files a/demo/frames_flat/frame_004.png and /dev/null differ
diff --git a/demo/frames_flat/frame_005.png b/demo/frames_flat/frame_005.png
deleted file mode 100644
index 265d905..0000000
Binary files a/demo/frames_flat/frame_005.png and /dev/null differ
diff --git a/demo/frames_flat/frame_006.png b/demo/frames_flat/frame_006.png
deleted file mode 100644
index 7d3be2f..0000000
Binary files a/demo/frames_flat/frame_006.png and /dev/null differ
diff --git a/demo/frames_flat/frame_007.png b/demo/frames_flat/frame_007.png
deleted file mode 100644
index e294978..0000000
Binary files a/demo/frames_flat/frame_007.png and /dev/null differ
diff --git a/demo/frames_flat/frame_008.png b/demo/frames_flat/frame_008.png
deleted file mode 100644
index fc9033b..0000000
Binary files a/demo/frames_flat/frame_008.png and /dev/null differ
diff --git a/demo/frames_flat/frame_009.png b/demo/frames_flat/frame_009.png
deleted file mode 100644
index 8401471..0000000
Binary files a/demo/frames_flat/frame_009.png and /dev/null differ
diff --git a/demo/frames_flat/frame_010.png b/demo/frames_flat/frame_010.png
deleted file mode 100644
index 531f3a0..0000000
Binary files a/demo/frames_flat/frame_010.png and /dev/null differ
diff --git a/demo/frames_flat/frame_011.png b/demo/frames_flat/frame_011.png
deleted file mode 100644
index 4c8abb3..0000000
Binary files a/demo/frames_flat/frame_011.png and /dev/null differ
diff --git a/demo/frames_flat/frame_012.png b/demo/frames_flat/frame_012.png
deleted file mode 100644
index 711b25a..0000000
Binary files a/demo/frames_flat/frame_012.png and /dev/null differ
diff --git a/demo/frames_flat/frame_013.png b/demo/frames_flat/frame_013.png
deleted file mode 100644
index cf14741..0000000
Binary files a/demo/frames_flat/frame_013.png and /dev/null differ
diff --git a/demo/frames_flat/frame_014.png b/demo/frames_flat/frame_014.png
deleted file mode 100644
index d856247..0000000
Binary files a/demo/frames_flat/frame_014.png and /dev/null differ
diff --git a/demo/frames_flat/frame_015.png b/demo/frames_flat/frame_015.png
deleted file mode 100644
index 33f7082..0000000
Binary files a/demo/frames_flat/frame_015.png and /dev/null differ
diff --git a/demo/frames_flat/frame_016.png b/demo/frames_flat/frame_016.png
deleted file mode 100644
index fe70b5f..0000000
Binary files a/demo/frames_flat/frame_016.png and /dev/null differ
diff --git a/demo/frames_flat/frame_017.png b/demo/frames_flat/frame_017.png
deleted file mode 100644
index b42fb62..0000000
Binary files a/demo/frames_flat/frame_017.png and /dev/null differ
diff --git a/demo/frames_flat/frame_018.png b/demo/frames_flat/frame_018.png
deleted file mode 100644
index 10f1d1c..0000000
Binary files a/demo/frames_flat/frame_018.png and /dev/null differ
diff --git a/demo/frames_flat/frame_019.png b/demo/frames_flat/frame_019.png
deleted file mode 100644
index c4228d0..0000000
Binary files a/demo/frames_flat/frame_019.png and /dev/null differ
diff --git a/demo/make_gif.py b/demo/make_gif.py
deleted file mode 100644
index 52fa96d..0000000
--- a/demo/make_gif.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env python3
-"""Stitch PNG frames into an animated GIF with progressive footstep visualization."""
-
-import glob
-import os
-import sys
-
-def make_gif(frame_dir, output_path, duration_per_frame=400, final_hold=2000):
- """Create GIF from frames, with longer hold on the last frame."""
- try:
- from PIL import Image
- except ImportError:
- print("Installing Pillow...")
- os.system(f"{sys.executable} -m pip install --break-system-packages Pillow")
- from PIL import Image
-
- frames = sorted(glob.glob(os.path.join(frame_dir, "frame_*.png")))
- if not frames:
- print(f"No frames found in {frame_dir}")
- sys.exit(1)
-
- print(f"Loading {len(frames)} frames...")
- images = [Image.open(f) for f in frames]
-
- # Hold the last frame longer
- durations = [duration_per_frame] * (len(images) - 1) + [final_hold]
-
- print(f"Saving GIF to {output_path}...")
- images[0].save(
- output_path,
- save_all=True,
- append_images=images[1:],
- duration=durations,
- loop=0,
- optimize=True,
- )
- size_kb = os.path.getsize(output_path) / 1024
- print(f"Done! {output_path} ({size_kb:.0f} KB)")
-
-
-if __name__ == "__main__":
- frame_dir = sys.argv[1] if len(sys.argv) > 1 else "demo/frames_flat"
- output = sys.argv[2] if len(sys.argv) > 2 else "demo/flat_terrain.gif"
- make_gif(frame_dir, output)
diff --git a/demo/obstacle_avoidance.gif b/demo/obstacle_avoidance.gif
new file mode 100644
index 0000000..7bda024
Binary files /dev/null and b/demo/obstacle_avoidance.gif differ
diff --git a/include/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.h b/include/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.h
index 2ba6a8a..1367613 100644
--- a/include/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.h
+++ b/include/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.h
@@ -140,6 +140,31 @@ class StepConstraintCheck
* @return true if collision detected, false otherwise
*/
bool isGoalPoseCollidedWithStairRegion(ljh::heuclid::Pose3D _goalPose,ljh::heuclid::ConvexPolygon2D stairPolygon);
+
+ /**
+ * @brief Check if a foot polygon overlaps with an obstacle polygon (full polygon-polygon intersection).
+ *
+ * Uses isConvexPolygonIntersect: checks foot vertices in obstacle AND obstacle vertices in foot
+ * AND edge midpoints — more robust than just checking foot vertices inside obstacle.
+ * Used for obstacle avoidance in stair/obstacle mode.
+ */
+ bool isFootPolygonCollidedWithPolygon(double stepX, double stepY, double stepYaw, enum StepFlag stepFlag,
+ ljh::heuclid::ConvexPolygon2D obstaclePolygon);
+
+ /**
+ * @brief Check if a foot polygon is fully contained within a terrain polygon.
+ *
+ * Uses isConvexPolygonContained: all foot vertices must be inside terrain polygon.
+ * Used for stair climbing where each foot must land on a single terrain patch.
+ */
+ bool isFootPolygonContainedInPolygon(double stepX, double stepY, double stepYaw, enum StepFlag stepFlag,
+ ljh::heuclid::ConvexPolygon2D terrainPolygon);
+
+ /** @brief DiscreteFootstep overload */
+ bool isFootPolygonCollidedWithPolygon(DiscreteFootstep stepToCheck, ljh::heuclid::ConvexPolygon2D obstaclePolygon);
+
+ /** @brief DiscreteFootstep overload */
+ bool isFootPolygonContainedInPolygon(DiscreteFootstep stepToCheck, ljh::heuclid::ConvexPolygon2D terrainPolygon);
};
diff --git a/include/FootstepPlannerLJH/parameters.h b/include/FootstepPlannerLJH/parameters.h
index 1eee50e..8642479 100644
--- a/include/FootstepPlannerLJH/parameters.h
+++ b/include/FootstepPlannerLJH/parameters.h
@@ -50,6 +50,10 @@ class parameters
static CONST double edgecost_w_h;
static CONST double edgecost_w_area;
static CONST double edgecost_w_static;
+ /** @brief Weight for body path deviation penalty in edge cost.
+ * Penalizes footsteps that stray from the ellipsoid body path.
+ * Default: 0.0 (disabled). Typical tuning: 3.0–15.0. */
+ static CONST double edgecost_w_pathdev;
//StepNodeExpansionCheck
static CONST double MaxStepReach;
@@ -92,6 +96,12 @@ class parameters
// constraints for stair alignment// load in the step expansion
static CONST bool isStairAlignMode;
+ /** @brief Enable ellipsoid body path following heuristic in A* search.
+ * When true, the planner uses computeFollowEllipsoidPath() to guide
+ * footsteps along the body path from start to goal.
+ * Independent of isStairAlignMode (no stair constraints applied).
+ * @note Requires Simple2DBodyPathHolder to be initialized via HeuristicCalculator. */
+ static CONST bool followBodyPath;
static CONST ljh::heuclid::ConvexPolygon2D stairPolygon;
static CONST double footPolygonExtendedLength;
@@ -105,6 +115,8 @@ class parameters
double getEdgeCostHeight(const parameters& param);
double getEdgeCostArea(const parameters& param);
double getEdgeCostStaticPerStep(const parameters& param);
+ /** @brief Get body path deviation edge cost weight. */
+ double getEdgeCostPathDev(const parameters& param);
double getMaxStepReach(const parameters& param);
double getMinStepLength(const parameters& param);
double getMaxStepLength(const parameters& param);
@@ -122,6 +134,8 @@ class parameters
double getGoalYawProximity(const parameters& param);
bool getDebugFlag(const parameters& param);
bool getStairAlignMode(const parameters& param);
+ /** @brief Check if body path following heuristic is enabled. */
+ bool getFollowBodyPath(const parameters& param);
ljh::heuclid::ConvexPolygon2D getStairPolygon(const parameters& param);
double getFootPolygonExtendedLength(const parameters& param);
double getHWPOfWalkDistacne(const parameters& param);
@@ -136,6 +150,8 @@ class parameters
void SetEdgeCostHeight(parameters& param, const double& change);
void SetEdgeCostArea(parameters& param, const double& change);
void SetEdgeCostStaticPerStep(parameters& param, const double& change);
+ /** @brief Set body path deviation edge cost weight. @param change Weight value (0.0 to disable). */
+ void SetEdgeCostPathDev(parameters& param, const double& change);
void SetMaxStepReach(parameters& param, const double& change);
void SetMinStepLength(parameters& param, const double& change);
void SetMaxStepLength(parameters& param, const double& change);
@@ -153,6 +169,8 @@ class parameters
void SetGoalYawProximity(parameters& param, const double& change);
void SetDebugFlag(parameters& param, const bool& change);
void SetStairAlignMode(parameters& param, const bool& change);
+ /** @brief Enable/disable body path following heuristic (independent of stair mode). */
+ void SetFollowBodyPath(parameters& param, const bool& change);
void SetStairPolygon(parameters& param, std::vector > stairBuffer, int numOfVertices, bool clockwiseOrdered);
void SetFootPolygonExtendedLength(const parameters& param, const double& change);
void SetHWPOfWalkDistacne(const parameters& param, const double& change);
diff --git a/lessons.md b/lessons.md
new file mode 100644
index 0000000..641c271
--- /dev/null
+++ b/lessons.md
@@ -0,0 +1,32 @@
+
+## 2026-03-17: Demo + Body Path Following Debugging
+
+### Issue 1: Body path heuristic tied to `isStairAlignMode`
+- **Problem**: `computeFollowEllipsoidPath` only called when `isStairAlignMode = true`, which also enables stair polygon constraints
+- **Fix**: Added `followBodyPath` static parameter — independent flag for body path heuristic
+- **Lesson**: Decouple orthogonal features. A flag controlling 2 unrelated behaviors is an anti-pattern.
+
+### Issue 2: `pfp.distance` is squared, not Euclidean
+- **Problem**: `Simple2DBodyPathHolder::getClosestdPointsYawfromPathToGivenPoint` stores `(X-x)²+(Y-y)²` in `pfp.distance`, but code compared it directly to `IdealStepWidth`
+- **Fix**: `std::abs(std::sqrt(this->pfp.distance) - IdealStepWidth)`
+- **Lesson**: Always check if a distance is squared. `sqrt()` is easy to miss in calling code.
+
+### Issue 3: Duplicate parameter override in demo
+- **Problem**: `SetFollowBodyPath(param, true)` on line 65, then `SetFollowBodyPath(param, false)` on line 80
+- **Root cause**: Incremental editing left duplicate code blocks
+- **Fix**: Remove duplicate section
+- **Lesson**: Static members are set once, later calls silently override. Grep for all occurrences before debugging.
+
+### Issue 4: Edge cost penalty too weak
+- **Problem**: `edgecost_w_pathdev = 3.0` added only ~0.18 per step vs walk cost of ~0.32 — negligible
+- **Insight**: Edge cost penalty has limited leverage. The heuristic is what truly guides the search.
+- **Lesson**: Fix the heuristic, not the edge cost, for path following behavior.
+
+### Issue 5: `isAnyVertexOfFootInsideStairRegion` incomplete for obstacle avoidance
+- **Problem**: Only checks if foot vertices are inside the obstacle polygon. Misses:
+ 1. Obstacle vertices inside foot rectangle (small obstacle near foot edge)
+ 2. Edge-edge intersection (foot straddles obstacle boundary — no vertex inside either polygon)
+- **Impact**: Foot and obstacle visually overlap in demo GIF but planner doesn't reject the step
+- **User requirement**: Full polygon-polygon collision detection (similar to `isTwoFootCollided` algorithm)
+- **Solution proposed**: Add `isPolygonCollided()` in `StepConstraintCheck` using 16-point mutual containment test (4 vertices + 4 edge midpoints × 2 directions)
+- **Lesson**: Point-in-polygon is necessary but NOT sufficient for convex polygon intersection. Need mutual containment + edge intersection for robustness.
diff --git a/src/FootstepPlannerLJH/AStarFootstepPlanner.cpp b/src/FootstepPlannerLJH/AStarFootstepPlanner.cpp
index e4d0236..24d76bb 100644
--- a/src/FootstepPlannerLJH/AStarFootstepPlanner.cpp
+++ b/src/FootstepPlannerLJH/AStarFootstepPlanner.cpp
@@ -96,7 +96,8 @@ void AStarFootstepPlanner::doAStarSearch()
this->costSoFarMap[next] = new_cost;
cost_t priority;
- if(this->param.isStairAlignMode)
+ // Use body path following heuristic when enabled (separate from stair mode)
+ if(this->param.getFollowBodyPath(this->param))
priority = new_cost + this->stepCostCalculator.computeHeuristicCostWithEllipsiodPath(next);
else
priority = new_cost + this->stepCostCalculator.computeHeuristicCost(next);
diff --git a/src/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.cpp b/src/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.cpp
index 1d358f9..862c327 100644
--- a/src/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.cpp
+++ b/src/FootstepPlannerLJH/StepConstraints/StepConstraintCheck.cpp
@@ -42,71 +42,43 @@ bool StepConstraintCheck::isAnyVertexOfFootInsideStairRegion(DiscreteFootstep st
stairPolygon.getVertexBuffer(),stairPolygon.getNumOfVertices(),stairPolygon.getClockwiseOrder());
}
-bool StepConstraintCheck::isTwoFootCollided(double stanceX, double stanceY, double stanceYaw, enum StepFlag stanceFlag,
- double swingX, double swingY, double swingYaw, enum StepFlag swingFlag)
-{
- //calculate and load the vertex2d(in clockwiseorder) of the stanceStep as polygon
- this->stepPose.setPosition(stanceX,stanceY);
- this->stepPose.setOrientation(stanceYaw);
- getExtendedFootVertex2D(this->stepPose,stanceFlag,this->vertexX8,this->vertexY8,this->param.footPolygonExtendedLength);
-
- this->stanceBuffer.resize(4);
- for(int i=0;i<4;i++)
- {
- this->vertex.setPoint2D(this->vertexX8.at(i),this->vertexY8.at(i));
- this->stanceBuffer[i] = this->vertex;
- }
- // calculate the vertex2d of the swingStep
- this->stepPose.setPosition(swingX,swingY);
- this->stepPose.setOrientation(swingYaw);
- getExtendedFootVertex2D(this->stepPose,swingFlag,this->vertexX,this->vertexY,this->param.footPolygonExtendedLength);
- // Four Vertex is not enough, need Eight Vertices
- //this->vertexX8.clear(); this->vertexY8.clear();
+// ============================================================================
+// Helper: Build foot ConvexPolygon2D from pose + side
+// ============================================================================
- // check each vertex of swingStep Whether in stanceStep polygon
- for(int i=0;i<4;i++)
- {
- // getFootVertex2D would make an clockwise order
- if(this->polygonTools.isPoint2DInsideConvexPolygon2D(this->vertexX.at(i),this->vertexY.at(i),this->stanceBuffer,4,1))
- return true;
- }
- // check 4 middle points of edge
- for(int i=0;i<4;i++)
- {
- if(this->polygonTools.isPoint2DInsideConvexPolygon2D(0.5*(this->vertexX.at(i)+this->vertexX.at((i+1)%4)),0.5*(this->vertexY.at(i)+this->vertexY.at((i+1)%4)),this->stanceBuffer,4,1))
- return true;
- }
-
-
- // check each vertex of stanceStep Whether in swingStep polygon
- this->stanceBuffer.resize(4);
- for(int i=0;i<4;i++)
- {
- this->vertex.setPoint2D(this->vertexX.at(i),this->vertexY.at(i));
- this->stanceBuffer[i] = this->vertex;
- }
+static ljh::heuclid::ConvexPolygon2D buildFootPolygon(double x, double y, double yaw,
+ enum StepFlag flag,
+ const parameters& param)
+{
+ ljh::heuclid::Pose2D pose;
+ pose.setPosition(x, y);
+ pose.setOrientation(yaw);
- for(int i=0;i<4;i++)
- {
- // getFootVertex2D would make an clockwise order
- if(this->polygonTools.isPoint2DInsideConvexPolygon2D(this->vertexX8.at(i),this->vertexY8.at(i),this->stanceBuffer,4,1))
- return true;
- }
+ std::vector vx, vy;
+ getExtendedFootVertex2D(pose, flag, vx, vy, param.footPolygonExtendedLength);
- // check 4 middle points of edge
- for(int i=0;i<4;i++)
+ std::vector> pts(4);
+ for(int i = 0; i < 4; i++)
{
- if(this->polygonTools.isPoint2DInsideConvexPolygon2D(0.5*(this->vertexX8.at(i)+this->vertexX8.at((i+1)%4)),0.5*(this->vertexY8.at(i)+this->vertexY8.at((i+1)%4)),this->stanceBuffer,4,1))
- return true;
+ pts[i].setPoint2D(vx.at(i), vy.at(i));
}
+ ljh::heuclid::ConvexPolygon2D poly(4);
+ poly.setVertexBuffer(pts);
+ poly.setClockwiseOrder(true);
+ return poly;
+}
+// ============================================================================
+// Refactored: Foot-foot collision using Heuclid isConvexPolygonIntersect
+// ============================================================================
-
- return false;
-
-
-
+bool StepConstraintCheck::isTwoFootCollided(double stanceX, double stanceY, double stanceYaw, enum StepFlag stanceFlag,
+ double swingX, double swingY, double swingYaw, enum StepFlag swingFlag)
+{
+ auto stancePoly = buildFootPolygon(stanceX, stanceY, stanceYaw, stanceFlag, this->param);
+ auto swingPoly = buildFootPolygon(swingX, swingY, swingYaw, swingFlag, this->param);
+ return this->polygonTools.isConvexPolygonIntersect(stancePoly, swingPoly);
}
bool StepConstraintCheck::isTwoFootCollided(DiscreteFootstep stanceStep, DiscreteFootstep swingStep)
@@ -229,4 +201,39 @@ bool StepConstraintCheck::isGoalPoseCollidedWithStairRegion(ljh::heuclid::Pose3D
double length = std::sqrt(std::pow(centralPoint.getX()-_goalPose.getPosition().getX(),2) + std::pow(centralPoint.getX()-_goalPose.getPosition().getX(),2));
return false;
}
+
+// ============================================================================
+// New: Foot-obstacle collision (full polygon-polygon intersection)
+// ============================================================================
+
+bool StepConstraintCheck::isFootPolygonCollidedWithPolygon(double stepX, double stepY, double stepYaw, enum StepFlag stepFlag,
+ ljh::heuclid::ConvexPolygon2D obstaclePolygon)
+{
+ auto footPoly = buildFootPolygon(stepX, stepY, stepYaw, stepFlag, this->param);
+ return this->polygonTools.isConvexPolygonIntersect(footPoly, obstaclePolygon);
+}
+
+bool StepConstraintCheck::isFootPolygonCollidedWithPolygon(DiscreteFootstep stepToCheck, ljh::heuclid::ConvexPolygon2D obstaclePolygon)
+{
+ return this->isFootPolygonCollidedWithPolygon(stepToCheck.getX(), stepToCheck.getY(), stepToCheck.getYaw(),
+ stepToCheck.getRobotSide().getStepFlag(), obstaclePolygon);
+}
+
+// ============================================================================
+// New: Foot-terrain containment (all foot vertices must be inside terrain)
+// ============================================================================
+
+bool StepConstraintCheck::isFootPolygonContainedInPolygon(double stepX, double stepY, double stepYaw, enum StepFlag stepFlag,
+ ljh::heuclid::ConvexPolygon2D terrainPolygon)
+{
+ auto footPoly = buildFootPolygon(stepX, stepY, stepYaw, stepFlag, this->param);
+ return this->polygonTools.isConvexPolygonContained(footPoly, terrainPolygon);
+}
+
+bool StepConstraintCheck::isFootPolygonContainedInPolygon(DiscreteFootstep stepToCheck, ljh::heuclid::ConvexPolygon2D terrainPolygon)
+{
+ return this->isFootPolygonContainedInPolygon(stepToCheck.getX(), stepToCheck.getY(), stepToCheck.getYaw(),
+ stepToCheck.getRobotSide().getStepFlag(), terrainPolygon);
+}
+
_FOOTSTEP_PLANNER_END
diff --git a/src/FootstepPlannerLJH/StepCost/FootstepCostCalculator.cpp b/src/FootstepPlannerLJH/StepCost/FootstepCostCalculator.cpp
index e1a2a47..12a49cf 100644
--- a/src/FootstepPlannerLJH/StepCost/FootstepCostCalculator.cpp
+++ b/src/FootstepPlannerLJH/StepCost/FootstepCostCalculator.cpp
@@ -3,6 +3,7 @@
#include
+#include
_FOOTSTEP_PLANNER_BEGIN
@@ -31,6 +32,17 @@ cost_t FootstepCostCalculator::computeEdgeCost(Location candidateNode, Location
//+this->zOffset * this->param.edgecost_w_h
+this->param.edgecost_w_static;
+ // Body path deviation penalty: penalizes footsteps that stray from the
+ // ellipsoid body path. Uses squared distance from getClosestdPointsYawfromPathToGivenPoint().
+ // Tuning: edgecost_w_pathdev ~3.0–15.0 for moderate guidance, higher for stronger tracking.
+ // @note pfp.distance is the squared Euclidean distance to the closest waypoint.
+ if(this->param.edgecost_w_pathdev > 0.0)
+ {
+ PointFromPathInfo pfp;
+ this->heuristicCalculator.pathHolder.getClosestdPointsYawfromPathToGivenPoint(candidateNode, pfp);
+ this->edgeCost += this->param.edgecost_w_pathdev * pfp.distance;
+ }
+
return this->edgeCost;
}
diff --git a/src/FootstepPlannerLJH/StepCost/HeuristicCalculator.cpp b/src/FootstepPlannerLJH/StepCost/HeuristicCalculator.cpp
index 91e50dc..d2ef63b 100644
--- a/src/FootstepPlannerLJH/StepCost/HeuristicCalculator.cpp
+++ b/src/FootstepPlannerLJH/StepCost/HeuristicCalculator.cpp
@@ -78,7 +78,8 @@ cost_t HeuristicCalculator::computeFollowEllipsoidPath(FootstepGraphNode& node)
this->midFootPose.getOrientation().getYaw()-this->goalPose.getOrientation().getYaw())) * 0.5 * PI * this->param.IdealStepWidth;
this->desireHeading = this->pathHolder.getClosestdPointsYawfromPathToGivenPoint(node,this->pfp);
- this->pathDistance = std::abs(this->pfp.distance-this->param.IdealStepWidth);
+ // pfp.distance is squared Euclidean distance; sqrt() required for actual distance
+ this->pathDistance = std::abs(std::sqrt(this->pfp.distance)-this->param.IdealStepWidth);
//this->heuristicCost = cost_t(this->param.AStarHeuristicWeight * (this->finalTurnDistance+this->walkDistance));
this->heuristicCost = cost_t(this->param.AStarHeuristicFinalWeight * this->param.HWPOfFinalFinalTurnDistacne * (this->finalTurnDistance)
@@ -93,7 +94,8 @@ cost_t HeuristicCalculator::computeFollowEllipsoidPath(FootstepGraphNode& node)
// double x = (goalPose.getPosition().getX()-startPose.getPosition().getX());
// this->desireHeading = this->midFootPose.getOrientation().shiftProperYaw(atan2(y,x));
this->desireHeading = this->pathHolder.getClosestdPointsYawfromPathToGivenPoint(node,this->pfp);
- this->pathDistance = std::abs(this->pfp.distance-this->param.IdealStepWidth);
+ // pfp.distance is squared Euclidean distance; sqrt() required for actual distance
+ this->pathDistance = std::abs(std::sqrt(this->pfp.distance)-this->param.IdealStepWidth);
// this->initialTurnDistance = std::abs(this->midFootPose.getOrientation().shiftProperYaw(
// this->midFootPose.getOrientation().getYaw()-this->desireHeading)) * 0.5 * PI * this->param.IdealStepWidth;
diff --git a/src/FootstepPlannerLJH/StepExpansion/ParameterBasedStepExpansion.cpp b/src/FootstepPlannerLJH/StepExpansion/ParameterBasedStepExpansion.cpp
index 642d687..f4deb96 100644
--- a/src/FootstepPlannerLJH/StepExpansion/ParameterBasedStepExpansion.cpp
+++ b/src/FootstepPlannerLJH/StepExpansion/ParameterBasedStepExpansion.cpp
@@ -120,11 +120,11 @@ void ParameterBasedStepExpansion::doFullExpansion(FootstepGraphNode nodeToExpand
midStepYaw = stepside.negateIfRightSide(this->yawOffsets[i]);
childStep = constructNodeInPreviousNodeFrame(midStepLength,midStepWidth,midStepYaw,nodeToExpand.getSecondStep());
- // add check whether the foot is in stair polygon
+ // Full polygon-polygon collision check against obstacle
if(this->param.isStairAlignMode)
{
if(this->stepConstraintChecker.isAnyVertexOfFootInsideStairRegion(childStep,this->param.stairPolygon))
- continue; //drop the childstep if it's in stair region cause it's unrealiazbile
+ continue; //drop the childstep if it collides with the obstacle polygon
}
diff --git a/src/FootstepPlannerLJH/parameters.cpp b/src/FootstepPlannerLJH/parameters.cpp
index 674d5eb..95da52e 100644
--- a/src/FootstepPlannerLJH/parameters.cpp
+++ b/src/FootstepPlannerLJH/parameters.cpp
@@ -17,6 +17,7 @@ CONST double parameters:: edgecost_w_yaw = 4;
CONST double parameters:: edgecost_w_h = 1;
CONST double parameters:: edgecost_w_area = 1;
CONST double parameters:: edgecost_w_static = 1;
+CONST double parameters:: edgecost_w_pathdev = 0.0;
// NodeCheck
CONST double parameters:: MaxStepReach = sqrt(0.26*0.26 + 0.2*0.2);//0.26;//(m)
@@ -58,6 +59,7 @@ CONST double parameters:: goalYawProximity = 5*pi/180;
CONST bool parameters:: debugFlag = true;
CONST bool parameters:: isStairAlignMode = false;
+CONST bool parameters:: followBodyPath = false;
CONST ljh::heuclid::ConvexPolygon2D parameters:: stairPolygon;
CONST double parameters:: footPolygonExtendedLength = 0.0;
@@ -70,6 +72,7 @@ double parameters:: getEdgeCostHeight(const parameters& param){ return param.edg
double parameters:: getEdgeCostYaw(const parameters& param){ return param.edgecost_w_d;}
double parameters:: getEdgeCostArea(const parameters& param){ return param.edgecost_w_area; }
double parameters:: getEdgeCostStaticPerStep(const parameters& param){ return param.edgecost_w_static; }
+double parameters:: getEdgeCostPathDev(const parameters& param){ return param.edgecost_w_pathdev; }
double parameters:: getMaxStepReach(const parameters& param){ return param.MaxStepReach; }
double parameters:: getMinStepLength(const parameters& param){ return param.MinStepLength; }
double parameters:: getMaxStepLength(const parameters& param){ return param.MaxStepLength; }
@@ -87,6 +90,7 @@ double parameters:: getGoalDistanceProximity(const parameters& param){ return pa
double parameters:: getGoalYawProximity(const parameters& param){ return param.goalYawProximity; }
bool parameters:: getDebugFlag(const parameters& param){ return param.debugFlag;}
bool parameters:: getStairAlignMode(const parameters& param){return param.isStairAlignMode; }
+bool parameters:: getFollowBodyPath(const parameters& param){return param.followBodyPath; }
ljh::heuclid::ConvexPolygon2D parameters:: getStairPolygon(const parameters& param){return param.stairPolygon;}
double parameters:: getFootPolygonExtendedLength(const parameters& param){return param.footPolygonExtendedLength;}
double parameters:: getHWPOfWalkDistacne(const parameters& param){return param.HWPOfWalkDistacne;}
@@ -102,6 +106,7 @@ void parameters::SetEdgeCostYaw(parameters& param, const double& change){ param.
void parameters::SetEdgeCostHeight(parameters& param, const double& change){ param.edgecost_w_h = change;}
void parameters::SetEdgeCostArea(parameters& param, const double& change){ param.edgecost_w_area = change;}
void parameters::SetEdgeCostStaticPerStep(parameters& param, const double& change){ param.edgecost_w_static = change;}
+void parameters::SetEdgeCostPathDev(parameters& param, const double& change){ param.edgecost_w_pathdev = change;}
void parameters::SetMaxStepReach(parameters& param, const double& change){ param.MaxStepReach = change;}
void parameters::SetMinStepLength(parameters& param, const double& change){ param.MinStepLength = change;}
void parameters::SetMaxStepLength(parameters& param, const double& change){ param.MaxStepLength = change;}
@@ -119,6 +124,7 @@ void parameters::SetGoalDistanceProximity(parameters& param, const double& chang
void parameters::SetGoalYawProximity(parameters& param, const double& change){ param.goalYawProximity = change;}
void parameters::SetDebugFlag(parameters& param, const bool& change){ param.debugFlag = change;}
void parameters::SetStairAlignMode(parameters& param, const bool& change){param.isStairAlignMode = change;}
+void parameters::SetFollowBodyPath(parameters& param, const bool& change){param.followBodyPath = change;}
void parameters::SetStairPolygon(parameters& param, std::vector > stairBuffer, int numOfVertices, bool clockwiseOrdered)
{
param.stairPolygon.setVertexBuffer(stairBuffer);