33 int samples_per_segment = 200)
35 if (waypoints.size() < 2)
36 throw std::invalid_argument(
"CatmullRomSpline requires at least 2 waypoints");
42 const auto& first = waypoints.front();
43 const auto& second = waypoints[1];
44 const auto& last = waypoints.back();
45 const auto& second_last = waypoints[waypoints.size() - 2];
47 points_.reserve(waypoints.size() + 2);
48 points_.push_back(2.0 * first - second);
49 for (
auto& p : waypoints)
50 points_.push_back(std::move(p));
51 points_.push_back(2.0 * last - second_last);
53 num_segments_ =
static_cast<int>(points_.size()) - 3;
54 buildArcLengthLUT(samples_per_segment);
58 [[nodiscard]]
double totalLength()
const {
return arc_length_lut_.back(); }
61 [[nodiscard]]
int numWaypoints()
const {
return num_segments_ + 1; }
68 auto [seg, t] = arcLengthToParam(s);
69 return evaluateSegment(seg, t);
75 [[nodiscard]] Eigen::Vector2d
tangentAt(
double s)
const
77 auto [seg, t] = arcLengthToParam(s);
78 Eigen::Vector2d d = evaluateSegmentDerivative(seg, t);
79 double norm = d.norm();
83 return Eigen::Vector2d(1.0, 0.0);
101 if (total < 1e-12)
return 0.0;
104 const double window = total * 0.25;
105 const double s_lo = std::max(0.0, s_hint - window);
106 const double s_hi = std::min(total, s_hint + window);
109 double best_s = s_hint;
110 double best_dist_sq = (
positionAt(s_hint) - point).squaredNorm();
112 const double ds = total /
static_cast<double>(arc_length_lut_.size() - 1);
113 int i_lo = std::max(0,
static_cast<int>(s_lo / ds));
114 int i_hi = std::min(
static_cast<int>(arc_length_lut_.size()) - 1,
115 static_cast<int>(s_hi / ds) + 1);
117 for (
int i = i_lo; i <= i_hi; ++i)
119 double si = arc_length_lut_[i];
120 double d2 = (
positionAt(si) - point).squaredNorm();
121 if (d2 < best_dist_sq)
132 const double newton_radius = window;
133 const double newton_lo = std::max(0.0, best_s - newton_radius);
134 const double newton_hi = std::min(total, best_s + newton_radius);
137 for (
int iter = 0; iter < 5; ++iter)
139 auto [seg, t] = arcLengthToParam(s);
140 Eigen::Vector2d p = evaluateSegment(seg, t);
141 Eigen::Vector2d dp = evaluateSegmentDerivative(seg, t);
142 double dp_norm = dp.norm();
143 if (dp_norm < 1e-12)
break;
145 Eigen::Vector2d diff = p - point;
146 double f_prime = 2.0 * diff.dot(dp);
148 double f_double_prime = 2.0 * dp.squaredNorm();
149 if (std::abs(f_double_prime) < 1e-12)
break;
151 double step = f_prime / f_double_prime;
152 s = std::clamp(s - step, newton_lo, newton_hi);
156 double newton_dist_sq = (
positionAt(s) - point).squaredNorm();
157 return (newton_dist_sq <= best_dist_sq) ? s : best_s;
162 static double knotValue(
const Eigen::Vector2d& p0,
const Eigen::Vector2d& p1,
166 double d = (p1 - p0).norm();
167 return t_prev + std::sqrt(std::max(d, 1e-12));
176 [[nodiscard]] Eigen::Vector2d evaluateSegment(
int seg,
double t)
const
178 const Eigen::Vector2d& P0 = points_[seg];
179 const Eigen::Vector2d& P1 = points_[seg + 1];
180 const Eigen::Vector2d& P2 = points_[seg + 2];
181 const Eigen::Vector2d& P3 = points_[seg + 3];
185 double t1 = knotValue(P0, P1, t0);
186 double t2 = knotValue(P1, P2, t1);
187 double t3 = knotValue(P2, P3, t2);
190 double u = t1 + t * (t2 - t1);
193 auto lerp = [](
const Eigen::Vector2d& a,
const Eigen::Vector2d& b,
194 double ta,
double tb,
double u_) -> Eigen::Vector2d
196 double denom = tb - ta;
197 if (std::abs(denom) < 1e-12)
return a;
198 double alpha = (u_ - ta) / denom;
199 return (1.0 - alpha) * a + alpha * b;
202 Eigen::Vector2d A1 = lerp(P0, P1, t0, t1, u);
203 Eigen::Vector2d A2 = lerp(P1, P2, t1, t2, u);
204 Eigen::Vector2d A3 = lerp(P2, P3, t2, t3, u);
206 Eigen::Vector2d B1 = lerp(A1, A2, t0, t2, u);
207 Eigen::Vector2d B2 = lerp(A2, A3, t1, t3, u);
209 return lerp(B1, B2, t1, t2, u);
217 [[nodiscard]] Eigen::Vector2d evaluateSegmentDerivative(
int seg,
double t)
const
219 constexpr double h = 1e-5;
220 double t_lo = std::max(0.0, t - h);
221 double t_hi = std::min(1.0, t + h);
222 double dt = t_hi - t_lo;
223 if (dt < 1e-12)
return Eigen::Vector2d(0.0, 0.0);
224 return (evaluateSegment(seg, t_hi) - evaluateSegment(seg, t_lo)) / dt;
230 void buildArcLengthLUT(
int samples_per_segment)
232 int total_samples = num_segments_ * samples_per_segment + 1;
233 arc_length_lut_.resize(total_samples);
234 param_lut_.resize(total_samples);
236 arc_length_lut_[0] = 0.0;
237 param_lut_[0] = {0, 0.0};
239 Eigen::Vector2d prev = evaluateSegment(0, 0.0);
242 for (
int seg = 0; seg < num_segments_; ++seg)
244 for (
int i = 1; i <= samples_per_segment; ++i)
246 double t =
static_cast<double>(i) / samples_per_segment;
247 Eigen::Vector2d p = evaluateSegment(seg, t);
248 double chord = (p - prev).norm();
249 arc_length_lut_[idx] = arc_length_lut_[idx - 1] + chord;
250 param_lut_[idx] = {seg, t};
260 [[nodiscard]] std::pair<int, double> arcLengthToParam(
double s)
const
265 auto it = std::lower_bound(arc_length_lut_.begin(), arc_length_lut_.end(), s);
266 int idx =
static_cast<int>(it - arc_length_lut_.begin());
268 if (idx == 0)
return param_lut_[0];
269 if (idx >=
static_cast<int>(arc_length_lut_.size()))
270 return param_lut_.back();
273 double s0 = arc_length_lut_[idx - 1];
274 double s1 = arc_length_lut_[idx];
275 double frac = (s1 > s0) ? (s - s0) / (s1 - s0) : 0.0;
277 auto [seg0, t0] = param_lut_[idx - 1];
278 auto [seg1, t1] = param_lut_[idx];
282 return {seg0, t0 + frac * (t1 - t0)};
287 return (frac < 0.5) ? param_lut_[idx - 1] : param_lut_[idx];
291 std::vector<Eigen::Vector2d> points_;
292 int num_segments_{0};
295 std::vector<double> arc_length_lut_;
296 std::vector<std::pair<int, double>> param_lut_;