135 lines
6.0 KiB
C#
135 lines
6.0 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
|
|
{
|
|
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);
|
|
}
|
|
}
|