using System; using System.Collections.Generic; using System.Collections.ObjectModel; using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Output; /// /// 将回溯原语装配为调用方可消费的稠密路径和包含式方向分段。 /// 装配器保留原语边界的换向双点,其余相邻重复位姿会被删除。 /// public sealed class CoarsePathAssembler { private const double DuplicateTolerance = 1e-8d; private readonly FootprintCollisionChecker _collisionChecker; /// 创建使用默认连续车体检查器的路径装配器。 public CoarsePathAssembler() : this(new FootprintCollisionChecker()) { } /// 创建使用指定连续车体检查器的路径装配器。 public CoarsePathAssembler(FootprintCollisionChecker collisionChecker) { _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); } /// /// 从已回溯的恒曲率原语构造稠密路径。 /// 参数:backtrackedPath 提供原语顺序;request 提供地图和车辆;path、segments 为成功时的只读输出。 /// 返回:首点可通过连续车体检查、每个原语积分点数据一致且方向分段完整覆盖时为 true;否则返回 false 且输出为空。 /// public bool TryAssemble(BacktrackedPath backtrackedPath, PlanningRequest request, out IReadOnlyList path, out IReadOnlyList segments, out string failureReason) { path = EmptyPath(); segments = EmptySegments(); failureReason = string.Empty; if (backtrackedPath == null || request == null || request.Map == null || request.Vehicle == null || !IsFinitePose(backtrackedPath.Start) || !IsTravelDirection(backtrackedPath.StartDirection) || !NumericGuard.IsFinite(backtrackedPath.StartCurvaturePerMeter)) { failureReason = "路径装配输入无效。"; return false; } if (!_collisionChecker.IsPoseCollisionFree(backtrackedPath.Start, request.Map, request.Vehicle, 0d, out double startClearanceMeters)) { failureReason = "回溯路径起点未通过连续车体检查。"; return false; } var points = new List(); TravelDirection currentDirection = backtrackedPath.Primitives.Count > 0 ? backtrackedPath.Primitives[0].Direction : backtrackedPath.StartDirection; double currentCurvature = request.StartVehicleCurvature; double normalizedStartHeading = AngleMath.NormalizeRadians(backtrackedPath.Start.Heading); if (!NumericGuard.IsFinite(normalizedStartHeading)) { failureReason = "回溯路径起点航向无效。"; return false; } points.Add(new CoarsePathPoint(backtrackedPath.Start.X, backtrackedPath.Start.Y, normalizedStartHeading, backtrackedPath.Start.Heading, 0d, currentDirection, currentCurvature, startClearanceMeters, false, CoarsePathPointSource.Start)); for (int primitiveIndex = 0; primitiveIndex < backtrackedPath.Primitives.Count; primitiveIndex++) { MotionPrimitive primitive = backtrackedPath.Primitives[primitiveIndex]; if (!IsValidPrimitive(primitive)) { failureReason = "回溯路径包含无效原语。"; return false; } CoarsePathPoint lastPoint = points[points.Count - 1]; if (primitive.Direction != lastPoint.Direction) { // 换向处的旧方向终点和新方向起点必须共存,二者位置、航向、弧长完全相同。 points.Add(new CoarsePathPoint(lastPoint.X, lastPoint.Y, lastPoint.Heading, lastPoint.UnwrappedHeading, lastPoint.ArcLength, primitive.Direction, primitive.CurvaturePerMeter, lastPoint.BodyClearance, true, CoarsePathPointSource.MotionPrimitive)); } Pose2D previousPose = primitive.Start; for (int pointIndex = 0; pointIndex < primitive.Points.Count; pointIndex++) { Pose2D pose = primitive.Points[pointIndex]; double bodyClearanceMeters = primitive.BodyClearancesMeters[pointIndex]; if (!IsFinitePose(pose) || !IsValidClearance(bodyClearanceMeters)) { failureReason = "原语积分点或净空无效。"; return false; } CoarsePathPoint previousPoint = points[points.Count - 1]; double arcIncrementMeters = CalculateArcIncrement(previousPose, pose, primitive.CurvaturePerMeter); if (!NumericGuard.IsFinite(arcIncrementMeters) || arcIncrementMeters <= 0d) { failureReason = "原语积分点未产生正弧长。"; return false; } if (IsSamePose(previousPoint, pose)) { // 非换向情况下不允许重复采样点泄露到对外路径。 previousPose = pose; continue; } double normalizedHeading = AngleMath.NormalizeRadians(pose.Heading); double headingDelta = AngleMath.ShortestSignedDifference(previousPoint.Heading, normalizedHeading); if (!NumericGuard.IsFinite(normalizedHeading) || !NumericGuard.IsFinite(headingDelta)) { failureReason = "原语积分点航向无法展开。"; return false; } CoarsePathPointSource source = primitive.IsGoalTruncation && pointIndex == primitive.Points.Count - 1 ? CoarsePathPointSource.GoalTruncation : CoarsePathPointSource.MotionPrimitive; points.Add(new CoarsePathPoint(pose.X, pose.Y, normalizedHeading, previousPoint.UnwrappedHeading + headingDelta, previousPoint.ArcLength + arcIncrementMeters, primitive.Direction, primitive.CurvaturePerMeter, bodyClearanceMeters, false, source)); previousPose = pose; } } if (points.Count == 0) { failureReason = "路径装配未产生起点。"; return false; } path = new ReadOnlyCollection(points); segments = BuildSegments(points); return true; } private static IReadOnlyList BuildSegments(IReadOnlyList points) { var segments = new List(); int startIndex = 0; TravelDirection direction = points[0].Direction; for (int index = 1; index < points.Count; index++) { if (points[index].Direction == direction) continue; segments.Add(new PathSegment(segments.Count, direction, startIndex, index - 1, points[startIndex].IsGearSwitchPoint, true)); startIndex = index; direction = points[index].Direction; } segments.Add(new PathSegment(segments.Count, direction, startIndex, points.Count - 1, points[startIndex].IsGearSwitchPoint, false)); return new ReadOnlyCollection(segments); } private static double CalculateArcIncrement(Pose2D from, Pose2D to, double curvaturePerMeter) { if (!IsFinitePose(from) || !IsFinitePose(to) || !NumericGuard.IsFinite(curvaturePerMeter)) return double.NaN; if (Math.Abs(curvaturePerMeter) < 1e-12d) { double deltaX = to.X - from.X; double deltaY = to.Y - from.Y; return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); } double headingDelta = AngleMath.ShortestSignedDifference(from.Heading, to.Heading); return Math.Abs(headingDelta / curvaturePerMeter); } private static bool IsValidPrimitive(MotionPrimitive primitive) { return primitive != null && primitive.Start != null && primitive.Points != null && primitive.BodyClearancesMeters != null && primitive.Points.Count == primitive.BodyClearancesMeters.Count && primitive.Points.Count > 0 && IsTravelDirection(primitive.Direction) && NumericGuard.IsFinite(primitive.CurvaturePerMeter) && NumericGuard.IsPositiveFinite(primitive.ActualLengthMeters); } private static bool IsSamePose(CoarsePathPoint point, Pose2D pose) { return Math.Abs(point.X - pose.X) <= DuplicateTolerance && Math.Abs(point.Y - pose.Y) <= DuplicateTolerance && Math.Abs(AngleMath.ShortestSignedDifference(point.Heading, pose.Heading)) <= DuplicateTolerance; } private static bool IsFinitePose(Pose2D pose) { return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); } private static bool IsTravelDirection(TravelDirection direction) { return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; } private static bool IsValidClearance(double clearanceMeters) { return !double.IsNaN(clearanceMeters) && clearanceMeters >= 0d; } private static IReadOnlyList EmptyPath() { return new ReadOnlyCollection(new List()); } private static IReadOnlyList EmptySegments() { return new ReadOnlyCollection(new List()); } }