using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Threading; using System.Threading.Tasks; using ClumsyCore; using ClumsyCore.DTools; using ClumsyCore.Interfaces; using ClumsyCore.Pilot; using FundamentalLib; using MDCSToolBox.Clumsy.Pilot; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.CoarsePath.Test; using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; using MultiWheelC.TrajectoryPlanning.Mapping; namespace MultiWheelC; /// /// 显式空图的粗路径规划测试入口。 /// 只负责创建纯规划请求;规划、取消与可视化均由共享执行器处理,不会向底盘发送任何命令。 /// [MovementTest(name = "粗路径规划-显式空图")] public sealed class CoarsePathExplicitEmptyTest : MovementTest { /// public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.ExplicitEmpty, "显式空图"); /// public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); } /// /// 单矩形绕行的粗路径规划测试入口。 /// [MovementTest(name = "粗路径规划-单矩形绕行")] // TODO:#在该测试下发生了红色矩形栅格碰撞但依然规划成功,需要进一步核实与确认 public sealed class CoarsePathRectangleDetourTest : MovementTest { /// public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.RectangleDetour, "单矩形绕行"); /// public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); } /// /// 手工圆形、矩形和 TwoLeg 快照组合的粗路径规划测试入口。 /// [MovementTest(name = "粗路径规划-多来源障碍")] public sealed class CoarsePathManualAndTwoLegTest : MovementTest { /// public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.ManualAndTwoLeg, "多来源障碍"); /// public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); } /// /// 重复输入地图缓存命中的粗路径规划测试入口。 /// [MovementTest(name = "粗路径规划-缓存命中")] public sealed class CoarsePathCacheHitTest : MovementTest { /// public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.CacheHit, "缓存命中"); /// public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); } /// /// 前进起步、倒车到达并显示换向点的粗路径规划测试入口。 /// [MovementTest(name = "粗路径规划-倒车换向")] public sealed class CoarsePathReverseGearSwitchTest : MovementTest { /// public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.ReverseGearSwitch, "倒车换向"); /// public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); } /// /// 障碍带完全隔开起终点的无解粗路径规划测试入口。 /// [MovementTest(name = "粗路径规划-无解")] public sealed class CoarsePathNoFeasiblePathTest : MovementTest { /// public override void Test() => CoarsePathPlanningTestRunner.RunScenario(CoarsePathTestScenario.NoFeasiblePath, "无解障碍带"); /// public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); } /// /// 使用当前 AMR 车身几何中心位姿和人工终点的粗路径规划演示入口。 /// 输入的 X/Y 使用世界 mm、航向使用 deg;进入规划核心前由场景工厂一次性转换为 m/rad。 /// [MovementTest(name = "粗路径规划")] public sealed class CoarsePathPlanningTest : MovementTest { private const int MaximumManualObstacleCount = 20; private static long _nextManualObstacleSnapshotVersion; /// /// 读取一次 AMR 当前世界位姿、手动终点和障碍物快照后启动规划。 /// 注意:getCartLocation 在无定位时可能阻塞;全部输入会在启动后台任务前冻结,不会被规划线程重复读取。 /// public override void Test() { try { var amrPose = DetourInterface.getCartLocation(); double goalXmm = ReadFiniteInput("粗路径终点 X(世界 mm)"); double goalYmm = ReadFiniteInput("粗路径终点 Y(世界 mm)"); double goalHeadingDeg = ReadFiniteInput("粗路径终点航向(世界 deg)"); TimeSpan searchTimeout = ReadPositiveTimeoutInput("粗路径规划总超时(秒,必须大于 0)"); IReadOnlyList obstacles = ReadManualObstacles(); long snapshotVersion = obstacles.Count == 0 ? 0L : Interlocked.Increment(ref _nextManualObstacleSnapshotVersion); CoarsePathPlanningJob job = CoarsePathScenarioFactory.CreateManualObstacleDemo( amrPose.x, amrPose.y, amrPose.th, goalXmm, goalYmm, goalHeadingDeg, obstacles, snapshotVersion); job.Configuration.SearchTimeout = searchTimeout; CoarsePathPlanningTestRunner.Run("AMR 位姿 + 手动终点 + 手动障碍物", job); } catch (Exception exception) { CoarsePathPlanningTestRunner.ShowInputFailure(exception); } } /// public override void TestStop() => CoarsePathPlanningTestRunner.Stop(); private static IReadOnlyList ReadManualObstacles() { int count = ReadBoundedIntegerInput("手动障碍物数量(0-20)", 0, MaximumManualObstacleCount); var obstacles = new List(count); for (int index = 0; index < count; index++) { string label = "障碍物 " + (index + 1); int kind = ReadBoundedIntegerInput(label + " 类型(1圆形,2矩形)", 1, 2); double centerXmm = ReadFiniteInput(label + " 中心 X(世界 mm)"); double centerYmm = ReadFiniteInput(label + " 中心 Y(世界 mm)"); if (kind == 1) { double radiusMm = ReadPositiveFiniteInput(label + " 半径 r(mm)"); obstacles.Add(ManualCoarsePathObstacle.Circle(centerXmm, centerYmm, radiusMm)); } else { double lengthXmm = ReadPositiveFiniteInput(label + " X方向长度(mm)"); double widthYmm = ReadPositiveFiniteInput(label + " Y方向宽度(mm)"); obstacles.Add(ManualCoarsePathObstacle.AxisAlignedRectangle(centerXmm, centerYmm, lengthXmm, widthYmm)); } } return obstacles; } private static int ReadBoundedIntegerInput(string prompt, int minimum, int maximum) { object raw = UI.GetInput(prompt); string text = Convert.ToString(raw, CultureInfo.CurrentCulture); int value; if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out value) && !int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) throw new ArgumentException("输入必须是整数:" + prompt); if (value < minimum || value > maximum) throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt); return value; } private static double ReadPositiveFiniteInput(string prompt) { double value = ReadFiniteInput(prompt); if (value <= 0d) throw new ArgumentOutOfRangeException(nameof(prompt), "输入必须为正数:" + prompt); return value; } private static TimeSpan ReadPositiveTimeoutInput(string prompt) { double timeoutSeconds = ReadFiniteInput(prompt); if (timeoutSeconds <= 0d) throw new ArgumentOutOfRangeException(nameof(prompt), "输入必须为正数:" + prompt); try { return TimeSpan.FromSeconds(timeoutSeconds); } catch (OverflowException) { throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt); } } private static double ReadFiniteInput(string prompt) { object raw = UI.GetInput(prompt); string text = Convert.ToString(raw, CultureInfo.CurrentCulture); double value; if (!double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) && !double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value)) throw new ArgumentException("输入必须是有限数字:" + prompt); if (double.IsNaN(value) || double.IsInfinity(value)) throw new ArgumentException("输入必须是有限数字:" + prompt); return value; } } /// /// 粗路径 MovementTest 的共享后台会话、停止和绘制实现。 /// 同一时刻只允许一个会话绘制;启动新会话或停止时会取消旧会话,但不会等待旧任务退出。 /// internal static class CoarsePathPlanningTestRunner { private const string PainterLayerName = "CoarsePathPlanningV1"; private const float MillimetersPerMeter = 1000f; private const int MaximumVisibleGridLines = 100; private static readonly object SessionSync = new object(); private static readonly CoarsePathPlanningService PlanningService = new CoarsePathPlanningService(); private static readonly Painter Painter = UI.GetPainter(PainterLayerName, true); private static CancellationTokenSource _activeCancellation; private static Task _activeTask; private static long _nextSessionId; private static long _activeSessionId; /// /// 固定场景启动时冻结的 AMR 位姿。规划后台不会重新读取定位,确保输入一致。 /// private sealed class AmrPoseSnapshot { public AmrPoseSnapshot(double xMillimeters, double yMillimeters, double headingDegrees) { EnsureFiniteAmrValue(xMillimeters, "X"); EnsureFiniteAmrValue(yMillimeters, "Y"); EnsureFiniteAmrValue(headingDegrees, "航向"); XMillimeters = xMillimeters; YMillimeters = yMillimeters; HeadingDegrees = headingDegrees; } public double XMillimeters { get; } public double YMillimeters { get; } public double HeadingDegrees { get; } public string DisplayText { get { return "AMR 起点:X=" + XMillimeters.ToString("F0", CultureInfo.InvariantCulture) + " mm,Y=" + YMillimeters.ToString("F0", CultureInfo.InvariantCulture) + " mm,航向=" + HeadingDegrees.ToString("F1", CultureInfo.InvariantCulture) + " deg"; } } } /// /// 创建指定固定场景并将其提交给共享后台服务。 /// internal static void RunScenario(CoarsePathTestScenario scenario, string scenarioName) { try { var pose = DetourInterface.getCartLocation(); if (ReferenceEquals(pose, null)) throw new ArgumentException("AMR 位姿为空。"); var snapshot = new AmrPoseSnapshot(pose.x, pose.y, pose.th); CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenario, snapshot.XMillimeters, snapshot.YMillimeters, snapshot.HeadingDegrees); Run(scenarioName, job, snapshot); } catch (Exception exception) { ShowInputFailure(new ArgumentException("AMR 位姿不可用:" + exception.Message, exception)); } } /// /// 提交已经冻结输入的一次规划请求。调用立即返回,结果只会由对应会话的完成回调绘制。 /// internal static void Run(string scenarioName, CoarsePathPlanningJob job) { Run(scenarioName, job, null); } private static void Run(string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose) { if (job == null) throw new ArgumentNullException(nameof(job)); var cancellation = new CancellationTokenSource(); CancellationTokenSource previousCancellation; long sessionId; lock (SessionSync) { previousCancellation = _activeCancellation; _activeCancellation = cancellation; _activeTask = null; sessionId = ++_nextSessionId; _activeSessionId = sessionId; } // 先发布新会话编号,再取消旧任务,避免旧完成回调覆盖新画面。 if (previousCancellation != null) previousCancellation.Cancel(); Painter.Clear(); DrawPending(scenarioName, job, amrPose); Task task = Task.Run(() => PlanningService.Plan(job, cancellation.Token)); lock (SessionSync) { if (_activeSessionId == sessionId) _activeTask = task; } _ = task.ContinueWith(completed => Finish(sessionId, scenarioName, job, amrPose, cancellation, completed), TaskScheduler.Default); } /// /// 取消当前会话并清空专用图层;不等待后台任务结束。 /// 已取消任务的完成回调只释放资源,不再记录或绘制结果。 /// internal static void Stop() { CancellationTokenSource cancellation; lock (SessionSync) { cancellation = _activeCancellation; _activeCancellation = null; _activeTask = null; _activeSessionId = 0; } if (cancellation != null) cancellation.Cancel(); Painter.Clear(); Hedingben.ToastText("粗路径规划已请求停止。", PainterLayerName); } /// /// 显示输入读取或校验失败,且不改变现有规划任务。 /// internal static void ShowInputFailure(Exception exception) { string message = exception == null ? "未知输入错误。" : exception.Message; Painter.DrawText(Color.LightYellow, "粗路径规划未启动:" + message, 0f, 0f); Hedingben.ToastText("粗路径规划未启动:" + message, PainterLayerName); } private static void Finish(long sessionId, string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose, CancellationTokenSource cancellation, Task completed) { try { CoarsePathPlanningJobResult result = completed.GetAwaiter().GetResult(); bool isCurrent; lock (SessionSync) { isCurrent = _activeSessionId == sessionId; if (isCurrent) { _activeTask = null; _activeCancellation = null; } } if (!isCurrent) return; DrawResult(scenarioName, job, amrPose, result); Hedingben.ToastText(BuildToastMessage(scenarioName, result), PainterLayerName); } catch (Exception exception) { bool isCurrent; lock (SessionSync) { isCurrent = _activeSessionId == sessionId; if (isCurrent) { _activeTask = null; _activeCancellation = null; } } if (isCurrent) Hedingben.ToastText("粗路径规划任务异常:" + exception.GetType().Name + "。" + exception.Message, PainterLayerName); } finally { cancellation.Dispose(); } } private static void DrawPending(string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose) { DrawPose(job.Start, Color.LimeGreen, "起点"); DrawPose(job.Goal, Color.Orange, "终点"); Painter.DrawText(Color.LightGray, "场景:" + scenarioName + "(规划中)", 0f, 0f); if (amrPose != null) Painter.DrawText(Color.LightGray, amrPose.DisplayText, 0f, -120f); } private static void DrawResult(string scenarioName, CoarsePathPlanningJob job, AmrPoseSnapshot amrPose, CoarsePathPlanningJobResult result) { Painter.Clear(); PlanningGridMap map = result.MapResult.Map; if (map != null) DrawMap(map); DrawPose(job.Start, Color.LimeGreen, "起点"); DrawGoal(job.Goal, job.Configuration, Color.Orange); if (result.PlanningResult.Status == PlanningStatus.Success) DrawPath(result.PlanningResult, job.Vehicle); DrawLegend(map); DrawStatus(scenarioName, job, result, map, amrPose); } private static void DrawMap(PlanningGridMap map) { float xMin = map.Bounds.XMin; float xMax = map.Bounds.XMax; float yMin = map.Bounds.YMin; float yMax = map.Bounds.YMax; float resolution = map.ResolutionMm; int gridStride = Math.Max(1, (int)Math.Ceiling(Math.Max(map.Rows, map.Cols) / (double)MaximumVisibleGridLines)); // 真实 ResolutionMm 决定网格位置,gridStride 只影响显示抽稀。 for (int col = 0; col <= map.Cols; col += gridStride) { float x = Math.Min(xMax, xMin + col * resolution); Painter.DrawLine(Color.FromArgb(80, Color.SlateGray), x, yMin, x, yMax, width: 1); } for (int row = 0; row <= map.Rows; row += gridStride) { float y = Math.Min(yMax, yMin + row * resolution); Painter.DrawLine(Color.FromArgb(80, Color.SlateGray), xMin, y, xMax, y, width: 1); } DrawRectangle(Color.Gainsboro, xMin, yMin, xMax, yMax, 3); if (xMin <= 0f && 0f < xMax) Painter.DrawLine(Color.DimGray, 0f, yMin, 0f, yMax, width: 2); if (yMin <= 0f && 0f < yMax) Painter.DrawLine(Color.DimGray, xMin, 0f, xMax, 0f, width: 2); for (int row = 0; row < map.Rows; row++) { for (int col = 0; col < map.Cols; col++) { if (!map.IsOccupied(row, col)) continue; float x = xMin + col * resolution + resolution / 2f; float y = yMin + row * resolution; Painter.DrawLine(Color.FromArgb(150, Color.Firebrick), x, y, x, y + resolution, width: Math.Max(1, (int)Math.Round(resolution))); } } } private static void DrawPose(Pose2D pose, Color color, string label) { if (pose == null) return; float x = ToMillimeters(pose.X); float y = ToMillimeters(pose.Y); Painter.DrawCircle(color, x, y, 80f); DrawHeadingArrow(x, y, pose.Heading, color, 260f); Painter.DrawText(color, label, x + 100f, y + 100f); } private static void DrawGoal(Pose2D goal, HybridAStarConfiguration configuration, Color color) { DrawPose(goal, color, "终点"); if (goal == null || configuration == null) return; Painter.DrawCircle(Color.FromArgb(150, color), ToMillimeters(goal.X), ToMillimeters(goal.Y), ToMillimeters(configuration.GoalPositionToleranceMeters)); } private static void DrawPath(PlanningResult planningResult, VehicleParameters vehicle) { if (planningResult.Path == null || planningResult.Path.Count == 0) return; int frameStride = Math.Max(1, planningResult.Path.Count / 10); for (int index = 1; index < planningResult.Path.Count; index++) { CoarsePathPoint previous = planningResult.Path[index - 1]; CoarsePathPoint current = planningResult.Path[index]; Color color = current.Direction == TravelDirection.Forward ? Color.LimeGreen : Color.DeepSkyBlue; Painter.DrawLine(color, ToMillimeters(previous.X), ToMillimeters(previous.Y), ToMillimeters(current.X), ToMillimeters(current.Y), width: 4); if (index % frameStride == 0 || current.IsGearSwitchPoint || index == planningResult.Path.Count - 1) DrawVehicleFrame(current, vehicle); if (index % Math.Max(1, frameStride / 2) == 0) DrawHeadingArrow(ToMillimeters(current.X), ToMillimeters(current.Y), current.Heading + (current.Direction == TravelDirection.Reverse ? Math.PI : 0d), color, 140f); if (!current.IsGearSwitchPoint) continue; float x = ToMillimeters(current.X); float y = ToMillimeters(current.Y); Painter.DrawCircle(Color.MediumPurple, x, y, 100f); Painter.DrawText(Color.MediumPurple, "换向", x + 110f, y - 110f); } DrawVehicleFrame(planningResult.Path[0], vehicle); } private static void DrawVehicleFrame(CoarsePathPoint point, VehicleParameters vehicle) { if (point == null || vehicle == null) return; float halfLength = ToMillimeters(vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters); float halfWidth = ToMillimeters(vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters); float centerX = ToMillimeters(point.X); float centerY = ToMillimeters(point.Y); double cos = Math.Cos(point.Heading); double sin = Math.Sin(point.Heading); TransformVehicleCorner(centerX, centerY, cos, sin, halfLength, halfWidth, out float frontLeftX, out float frontLeftY); TransformVehicleCorner(centerX, centerY, cos, sin, halfLength, -halfWidth, out float frontRightX, out float frontRightY); TransformVehicleCorner(centerX, centerY, cos, sin, -halfLength, -halfWidth, out float rearRightX, out float rearRightY); TransformVehicleCorner(centerX, centerY, cos, sin, -halfLength, halfWidth, out float rearLeftX, out float rearLeftY); Painter.DrawLine(Color.Gold, frontLeftX, frontLeftY, frontRightX, frontRightY, width: 2); Painter.DrawLine(Color.Gold, frontRightX, frontRightY, rearRightX, rearRightY, width: 2); Painter.DrawLine(Color.Gold, rearRightX, rearRightY, rearLeftX, rearLeftY, width: 2); Painter.DrawLine(Color.Gold, rearLeftX, rearLeftY, frontLeftX, frontLeftY, width: 2); } private static void TransformVehicleCorner(float centerX, float centerY, double cos, double sin, float longitudinal, float lateral, out float x, out float y) { x = centerX + (float)(cos * longitudinal - sin * lateral); y = centerY + (float)(sin * longitudinal + cos * lateral); } private static void DrawHeadingArrow(float x, float y, double headingRadians, Color color, float length) { float endX = x + (float)Math.Cos(headingRadians) * length; float endY = y + (float)Math.Sin(headingRadians) * length; Painter.DrawLine(color, x, y, endX, endY, endArrow: true, width: 3); } private static void DrawLegend(PlanningGridMap map) { float x = map == null ? 0f : map.Bounds.XMin + 150f; float y = map == null ? 250f : map.Bounds.YMax - 180f; Painter.DrawText(Color.White, "图例", x, y); DrawLegendItem(x, y - 130f, Color.Gainsboro, "边界 / 栅格"); DrawLegendItem(x, y - 260f, Color.Firebrick, "占据格"); DrawLegendItem(x, y - 390f, Color.LimeGreen, "起点 / 前进"); DrawLegendItem(x, y - 520f, Color.Orange, "终点 / 容差"); DrawLegendItem(x, y - 650f, Color.DeepSkyBlue, "倒车"); DrawLegendItem(x, y - 780f, Color.MediumPurple, "换向"); DrawLegendItem(x, y - 910f, Color.Gold, "扩大车体检查框"); } private static void DrawLegendItem(float x, float y, Color color, string text) { Painter.DrawLine(color, x, y, x + 90f, y, width: 5); Painter.DrawText(color, text, x + 120f, y - 30f); } private static void DrawStatus(string scenarioName, CoarsePathPlanningJob job, CoarsePathPlanningJobResult result, PlanningGridMap map, AmrPoseSnapshot amrPose) { float x = map == null ? 0f : map.Bounds.XMin + 150f; float y = map == null ? -250f : map.Bounds.YMin + 150f; string snapshot = map == null ? "无" : map.SnapshotId.ToString(CultureInfo.InvariantCulture); string resolution = map == null ? "无" : map.ResolutionMm.ToString("F0", CultureInfo.InvariantCulture) + " mm"; PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics; string reason = diagnostics.TerminationReason ?? string.Empty; string turningRadius = "无"; if (job != null && VehicleKinematics.TryGetMaximumCurvaturePerMeter(job.Vehicle, out double maximumCurvaturePerMeter)) turningRadius = (1d / maximumCurvaturePerMeter).ToString("F2", CultureInfo.InvariantCulture) + " m"; Painter.DrawText(Color.White, "场景:" + scenarioName, x, y); float detailOffset = 0f; if (amrPose != null) { Painter.DrawText(Color.White, amrPose.DisplayText, x, y + 120f); detailOffset = 120f; } Painter.DrawText(Color.White, "地图:" + result.MapResult.Status + ",缓存:" + result.MapResult.CacheHit + ",快照:" + snapshot, x, y + 120f + detailOffset); Painter.DrawText(Color.White, "栅格:" + resolution + ",规划:" + result.PlanningResult.Status + ",总耗时:" + diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + " ms,路径搜索:" + diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + " ms", x, y + 240f + detailOffset); Painter.DrawText(Color.White, "节点:扩展=" + diagnostics.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + ",生成=" + diagnostics.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + ",Open List峰值=" + diagnostics.PeakOpenListCount.ToString(CultureInfo.InvariantCulture), x, y + 360f + detailOffset); if (job != null && job.Vehicle != null) { Painter.DrawText(Color.White, "演示车辆:长=" + job.Vehicle.LengthMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,宽=" + job.Vehicle.WidthMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,余量=" + job.Vehicle.SafetyMarginMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,最小转弯半径=" + turningRadius, x, y + 480f + detailOffset); } if (!string.IsNullOrEmpty(reason)) Painter.DrawText(Color.LightYellow, "原因:" + reason, x, y + 600f + detailOffset); } private static string BuildToastMessage(string scenarioName, CoarsePathPlanningJobResult result) { PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics; string message = "粗路径[" + scenarioName + "]:地图=" + result.MapResult.Status + ",规划=" + result.PlanningResult.Status + ",总耗时=" + diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + "ms,路径搜索=" + diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + "ms"; if (result.PlanningResult.Status != PlanningStatus.Success && !string.IsNullOrEmpty(diagnostics.TerminationReason)) message += ",原因=" + diagnostics.TerminationReason; return message + "。"; } private static void DrawRectangle(Color color, float xMin, float yMin, float xMax, float yMax, int width) { Painter.DrawLine(color, xMin, yMin, xMax, yMin, width: width); Painter.DrawLine(color, xMax, yMin, xMax, yMax, width: width); Painter.DrawLine(color, xMax, yMax, xMin, yMax, width: width); Painter.DrawLine(color, xMin, yMax, xMin, yMin, width: width); } private static float ToMillimeters(double meters) => (float)(meters * MillimetersPerMeter); private static void EnsureFiniteAmrValue(double value, string name) { if (double.IsNaN(value) || double.IsInfinity(value)) throw new ArgumentException(name + " 必须是有限数。"); } }