141 lines
6.6 KiB
C#
141 lines
6.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
|
|
using MultiWheelC.TrajectoryPlanning.Utils;
|
|
|
|
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2;
|
|
|
|
/// <summary>将已验证的局部候选替换进同方向预处理路径;保持窗口外点、端点和换向拓扑不变。</summary>
|
|
internal sealed class LocalG2PathSplicer
|
|
{
|
|
/// <summary>用候选窗口点替换目标方向段的局部弧长区间。</summary>
|
|
/// <param name="currentPath">当前已接受替换的预处理路径。</param>
|
|
/// <param name="candidate">候选替换几何;位置和局部弧长单位为 m。</param>
|
|
/// <param name="replacedPath">成功时为保持其他段与边界不变的新预处理路径;失败时为 <see langword="null"/>。</param>
|
|
/// <param name="reason">失败原因;成功时为空字符串。</param>
|
|
/// <returns>替换范围、端点和方向拓扑均合法时为 <see langword="true"/>;否则为 <see langword="false"/>。</returns>
|
|
internal bool TryReplace(
|
|
PreparedPath currentPath,
|
|
LocalG2CandidateGeometry candidate,
|
|
out PreparedPath replacedPath,
|
|
out string reason)
|
|
{
|
|
replacedPath = null;
|
|
reason = string.Empty;
|
|
if (currentPath == null || candidate == null || candidate.SegmentIndex < 0 ||
|
|
candidate.SegmentIndex >= currentPath.Segments.Count || candidate.RegionPoints == null ||
|
|
candidate.RegionPoints.Count < 2)
|
|
{
|
|
reason = "局部 G2 拼接输入无效。";
|
|
return false;
|
|
}
|
|
|
|
PreparedDirectionSegment source = currentPath.Segments[candidate.SegmentIndex];
|
|
if (source == null || source.SegmentIndex != candidate.SegmentIndex ||
|
|
!PathReferenceInterpolator.TryInterpolateByArcLength(source.Points, candidate.StartArcLengthMeters, out SmoothingPoint2D start, out reason) ||
|
|
!PathReferenceInterpolator.TryInterpolateByArcLength(source.Points, candidate.EndArcLengthMeters, out SmoothingPoint2D end, out reason) ||
|
|
!SamePosition(candidate.RegionPoints[0], start) || !SamePosition(candidate.RegionPoints[candidate.RegionPoints.Count - 1], end))
|
|
{
|
|
if (string.IsNullOrEmpty(reason)) reason = "局部 G2 候选端点不匹配原始窗口。";
|
|
return false;
|
|
}
|
|
|
|
var combined = new List<SmoothingPoint2D>();
|
|
for (int index = 0; index < source.Points.Count; index++)
|
|
{
|
|
SmoothingPoint2D point = source.Points[index];
|
|
if (point.ArcLength < candidate.StartArcLengthMeters) AddWithoutNonGearDuplicates(combined, point);
|
|
}
|
|
// Candidate endpoints are only validated within a numerical tolerance. The replacement
|
|
// itself must use the exact source-window endpoints, including a possible gear marker.
|
|
AddWithoutNonGearDuplicates(combined, start);
|
|
for (int index = 1; index < candidate.RegionPoints.Count - 1; index++)
|
|
AddWithoutNonGearDuplicates(combined, candidate.RegionPoints[index]);
|
|
AddWithoutNonGearDuplicates(combined, end);
|
|
for (int index = 0; index < source.Points.Count; index++)
|
|
{
|
|
SmoothingPoint2D point = source.Points[index];
|
|
if (point.ArcLength > candidate.EndArcLengthMeters) AddWithoutNonGearDuplicates(combined, point);
|
|
}
|
|
if (!TryRecalculateArcLengths(combined, out IReadOnlyList<SmoothingPoint2D> localPoints, out reason)) return false;
|
|
|
|
var segments = new List<PreparedDirectionSegment>(currentPath.Segments.Count);
|
|
for (int index = 0; index < currentPath.Segments.Count; index++)
|
|
{
|
|
PreparedDirectionSegment segment = currentPath.Segments[index];
|
|
if (index != candidate.SegmentIndex)
|
|
{
|
|
segments.Add(segment);
|
|
continue;
|
|
}
|
|
segments.Add(new PreparedDirectionSegment(segment.SegmentIndex, segment.Direction, localPoints,
|
|
segment.StartsAtGearSwitch, segment.EndsAtGearSwitch, segment.StartVehicleCurvaturePerMeter));
|
|
}
|
|
replacedPath = new PreparedPath(segments);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryRecalculateArcLengths(
|
|
IReadOnlyList<SmoothingPoint2D> points,
|
|
out IReadOnlyList<SmoothingPoint2D> recalculated,
|
|
out string reason)
|
|
{
|
|
recalculated = null;
|
|
reason = string.Empty;
|
|
if (points == null || points.Count < 2)
|
|
{
|
|
reason = "局部 G2 拼接后方向段没有足够点。";
|
|
return false;
|
|
}
|
|
var output = new List<SmoothingPoint2D>(points.Count);
|
|
double arcLength = 0d;
|
|
for (int index = 0; index < points.Count; index++)
|
|
{
|
|
SmoothingPoint2D point = points[index];
|
|
if (!IsValid(point))
|
|
{
|
|
reason = "局部 G2 拼接点包含非法数值。";
|
|
return false;
|
|
}
|
|
if (index > 0)
|
|
{
|
|
double distance = Distance(points[index - 1], point);
|
|
if (!NumericGuard.IsPositiveFinite(distance))
|
|
{
|
|
reason = "局部 G2 拼接后存在重复非换向点。";
|
|
return false;
|
|
}
|
|
arcLength += distance;
|
|
}
|
|
output.Add(new SmoothingPoint2D(point.X, point.Y, arcLength, point.Heading, point.UnwrappedHeading,
|
|
point.BodyClearance, point.IsGearSwitchPoint, point.Source));
|
|
}
|
|
recalculated = output;
|
|
return true;
|
|
}
|
|
|
|
private static void AddWithoutNonGearDuplicates(List<SmoothingPoint2D> output, SmoothingPoint2D point)
|
|
{
|
|
if (output.Count == 0)
|
|
{
|
|
output.Add(point);
|
|
return;
|
|
}
|
|
SmoothingPoint2D previous = output[output.Count - 1];
|
|
if (!previous.IsGearSwitchPoint && !point.IsGearSwitchPoint && SamePosition(previous, point)) return;
|
|
output.Add(point);
|
|
}
|
|
|
|
private static bool IsValid(SmoothingPoint2D point) => point != null && NumericGuard.IsFinite(point.X) &&
|
|
NumericGuard.IsFinite(point.Y) && NumericGuard.IsFinite(point.Heading) && NumericGuard.IsFinite(point.UnwrappedHeading) &&
|
|
NumericGuard.IsFinite(point.BodyClearance) && point.BodyClearance >= 0d;
|
|
private static bool SamePosition(SmoothingPoint2D left, SmoothingPoint2D right) =>
|
|
left != null && right != null && Distance(left, right) <= 1e-9d;
|
|
private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right)
|
|
{
|
|
double dx = right.X - left.X;
|
|
double dy = right.Y - left.Y;
|
|
return Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
}
|