using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; using MultiWheelC.TrajectoryPlanning.CoarsePath.Test; using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; using MultiWheelC.TrajectoryPlanning.Mapping; using Newtonsoft.Json; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; /// 显式开发入口:从已成功的 Hybrid A* 粗路径结果生成八个稳定的快速比较夹具。 public static class SmoothingFixtureGenerator { // 仅用于开发时固化真实规划输出;不会改变业务场景或运行时服务的预算。 private static readonly TimeSpan FixtureGenerationSearchTimeout = TimeSpan.FromSeconds(120d); /// 生成夹具 JSON;目标已存在且未明确允许覆盖时拒绝写入。 public static void Generate(string outputPath, bool overwrite) { if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("输出路径不能为空。", nameof(outputPath)); if (File.Exists(outputPath) && !overwrite) throw new IOException("夹具目标已存在;必须显式允许覆盖。"); SmoothingFixtureDocument document = CreateDocument(); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath))); var settings = new JsonSerializerSettings { Culture = CultureInfo.InvariantCulture, Formatting = Formatting.Indented }; File.WriteAllText(outputPath, JsonConvert.SerializeObject(document, settings), new System.Text.UTF8Encoding(false)); } private static SmoothingFixtureDocument CreateDocument() { var document = new SmoothingFixtureDocument { SchemaVersion = 1 }; var planner = new CoarsePathPlanningService(); IReadOnlyList plans = CreatePlans(); for (int index = 0; index < plans.Count; index++) { FixturePlan plan = plans[index]; CoarsePathPlanningJob job = plan.CreateJob(); job.Configuration.SearchTimeout = FixtureGenerationSearchTimeout; CoarsePathPlanningJobResult result = planner.Plan(job, CancellationToken.None); document.Scenarios.Add(CreateRecord(plan.Id, job, result)); } for (int index = 0; index < document.Scenarios.Count; index++) document.Scenarios[index].ConfigurationFingerprint = SmoothingScenarioFixtureLoader.ComputeFingerprint(document.Scenarios[index]); return document; } private static IReadOnlyList CreatePlans() { return new List { new FixturePlan("straight", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ExplicitEmpty)), new FixturePlan("single-turn", () => CoarsePathScenarioFactory.CreateManualGoalDemo(1000d, 1000d, 0d, 3000d, 2000d, 45d)), new FixturePlan("s-bend", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ManualAndTwoLeg, 1000d, 1000d, 45d)), new FixturePlan("large-heading-change", () => CoarsePathScenarioFactory.CreateManualGoalDemo(1000d, 1000d, 0d, 3000d, 3000d, 90d)), new FixturePlan("rectangle-detour", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.RectangleDetour)), new FixturePlan("multi-obstacle-detour", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ManualAndTwoLeg)), new FixturePlan("narrow-corridor", CreateNarrowCorridor), new FixturePlan("forward-reverse-switch", () => CoarsePathScenarioFactory.Create(CoarsePathTestScenario.ReverseGearSwitch)), }; } private static CoarsePathPlanningJob CreateNarrowCorridor() { return CreateFixedObstacleJob("narrow-corridor", new IMapObstacle[] { new AxisAlignedRectangleObstacle(1500f, 4500f, 500f, 1200f), new AxisAlignedRectangleObstacle(1500f, 4500f, 2800f, 3500f), }); } private static CoarsePathPlanningJob CreateFixedObstacleJob(string id, IReadOnlyList obstacles) { var job = new CoarsePathPlanningJob { MapRequest = new PlanningMapRequest { Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), ResolutionMm = 50f, AllowExplicitEmptyMap = false, ObstacleSources = new IMapObstacleSource[] { new ManualObstacleSource("fixture-" + id, 1L, true, obstacles), }, }, Start = new Pose2D(1d, 2d, 0d), Goal = new Pose2D(5d, 2d, 0d), Vehicle = new VehicleParameters { LengthMeters = 0.80d, WidthMeters = 0.60d, SafetyMarginMeters = 0.05d, MaximumCurvaturePerMeter = 1d / 1.20d, }, Configuration = new HybridAStarConfiguration(), GoalDirection = GoalDirectionConstraint.Forward, }; job.Configuration.SearchTimeout = TimeSpan.FromSeconds(30d); return job; } private static SmoothingFixtureRecord CreateRecord(string id, CoarsePathPlanningJob job, CoarsePathPlanningJobResult result) { if (job == null || result == null || result.MapResult == null || !result.MapResult.Succeeded || result.MapResult.Map == null || result.PlanningResult == null || result.PlanningResult.Status != PlanningStatus.Success) { string status = result == null || result.PlanningResult == null ? "no-result" : result.PlanningResult.Status.ToString(); string reason = result == null || result.PlanningResult == null || result.PlanningResult.Diagnostics == null ? string.Empty : result.PlanningResult.Diagnostics.TerminationReason; throw new InvalidOperationException("夹具场景未产生成功的粗路径:" + id + ";状态=" + status + ";原因=" + reason); } PlanningGridMap map = result.MapResult.Map; IReadOnlyList smoothingPath = SmoothingScenarioFactory.CopyWithFiniteClearance(result.PlanningResult.Path, map); var record = new SmoothingFixtureRecord { Id = id, FixtureVersion = 1, Map = CreateMapRecord(map, result.MapResult.SourceResults), Vehicle = CreateVehicleRecord(job.Vehicle), PlanningConfiguration = CreatePlanningConfigurationRecord(job.Configuration), }; for (int index = 0; index < smoothingPath.Count; index++) record.Path.Add(CreatePathPointRecord(smoothingPath[index])); for (int index = 0; index < result.PlanningResult.Segments.Count; index++) record.Segments.Add(CreateSegmentRecord(result.PlanningResult.Segments[index])); return record; } private static SmoothingFixtureMap CreateMapRecord(PlanningGridMap map, IReadOnlyList sourceResults) { var record = new SmoothingFixtureMap { XMinMm = map.Bounds.XMin, XMaxMm = map.Bounds.XMax, YMinMm = map.Bounds.YMin, YMaxMm = map.Bounds.YMax, ResolutionMm = map.ResolutionMm, }; for (int sourceIndex = 0; sourceIndex < sourceResults.Count; sourceIndex++) { IReadOnlyList obstacles = sourceResults[sourceIndex].Obstacles; for (int obstacleIndex = 0; obstacleIndex < obstacles.Count; obstacleIndex++) record.Obstacles.Add(CreateObstacleRecord(obstacles[obstacleIndex])); } return record; } private static SmoothingFixtureObstacle CreateObstacleRecord(IMapObstacle obstacle) { if (obstacle is CircleObstacle circle) { return new SmoothingFixtureObstacle { Kind = "circle", CenterXMm = circle.CenterX, CenterYMm = circle.CenterY, RadiusMm = circle.RadiusMm, }; } if (obstacle is AxisAlignedRectangleObstacle rectangle) { return new SmoothingFixtureObstacle { Kind = "axis-aligned-rectangle", XMinMm = rectangle.XMin, XMaxMm = rectangle.XMax, YMinMm = rectangle.YMin, YMaxMm = rectangle.YMax, }; } throw new InvalidOperationException("夹具仅支持圆形和轴对齐矩形障碍物。" + obstacle?.GetType().FullName); } private static SmoothingFixtureVehicle CreateVehicleRecord(VehicleParameters vehicle) { if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) throw new InvalidOperationException("夹具车辆缺少有效最大曲率限制。"); return new SmoothingFixtureVehicle { LengthMeters = vehicle.LengthMeters, WidthMeters = vehicle.WidthMeters, SafetyMarginMeters = vehicle.SafetyMarginMeters, MaximumCurvaturePerMeter = maximumCurvaturePerMeter, }; } private static SmoothingFixturePlanningConfiguration CreatePlanningConfigurationRecord(HybridAStarConfiguration configuration) { return new SmoothingFixturePlanningConfiguration { PrimitiveLengthMeters = configuration.PrimitiveLengthMeters, IntegrationStepMeters = configuration.IntegrationStepMeters, MaximumCollisionCheckStepMeters = configuration.MaximumCollisionCheckStepMeters, HeadingResolutionRadians = configuration.HeadingResolutionRadians, CurvatureLevelCount = configuration.CurvatureLevelCount, GoalPositionToleranceMeters = configuration.GoalPositionToleranceMeters, GoalHeadingToleranceRadians = configuration.GoalHeadingToleranceRadians, MaximumExpandedNodes = configuration.MaximumExpandedNodes, SearchTimeoutSeconds = configuration.SearchTimeout.TotalSeconds, HeuristicWeight = configuration.HeuristicWeight, ReverseCostMultiplier = configuration.ReverseCostMultiplier, GearSwitchPenaltyMeters = configuration.GearSwitchPenaltyMeters, CurvatureMagnitudeWeight = configuration.CurvatureMagnitudeWeight, CurvatureChangePenaltyMetersPerLevel = configuration.CurvatureChangePenaltyMetersPerLevel, ClearanceCostWeight = configuration.ClearanceCostWeight, ClearanceCostDistanceMeters = configuration.ClearanceCostDistanceMeters, AllowReverse = configuration.AllowReverse, }; } private static SmoothingFixturePathPoint CreatePathPointRecord(CoarsePathPoint point) { return new SmoothingFixturePathPoint { XMeters = point.X, YMeters = point.Y, HeadingRadians = point.Heading, UnwrappedHeadingRadians = point.UnwrappedHeading, ArcLengthMeters = point.ArcLength, Direction = point.Direction, VehicleCurvaturePerMeter = point.VehicleCurvature, BodyClearanceMeters = point.BodyClearance, IsGearSwitchPoint = point.IsGearSwitchPoint, Source = point.Source, }; } private static SmoothingFixtureSegment CreateSegmentRecord(PathSegment segment) { return new SmoothingFixtureSegment { SegmentIndex = segment.SegmentIndex, Direction = segment.Direction, StartIndex = segment.StartIndex, EndIndex = segment.EndIndex, StartsAtGearSwitch = segment.StartsAtGearSwitch, EndsAtGearSwitch = segment.EndsAtGearSwitch, }; } private sealed class FixturePlan { public FixturePlan(string id, Func createJob) { Id = id; CreateJob = createJob; } public string Id { get; } public Func CreateJob { get; } } }